diff --git a/abis/IAaveFlashLoanReceiver.json b/abis/IAaveFlashLoanReceiver.json new file mode 100644 index 0000000..5b28521 --- /dev/null +++ b/abis/IAaveFlashLoanReceiver.json @@ -0,0 +1,41 @@ +[ + { + "inputs": [ + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "premiums", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "initiator", + "type": "address" + }, + { + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "executeOperation", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/abis/IAaveLendPool.json b/abis/IAaveLendPool.json new file mode 100644 index 0000000..ed55802 --- /dev/null +++ b/abis/IAaveLendPool.json @@ -0,0 +1,58 @@ +[ + { + "inputs": [], + "name": "FLASHLOAN_PREMIUM_TOTAL", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiverAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "modes", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "internalType": "bytes", + "name": "params", + "type": "bytes" + }, + { + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/abis/IAaveLendPoolAddressesProvider.json b/abis/IAaveLendPoolAddressesProvider.json new file mode 100644 index 0000000..f16ddd3 --- /dev/null +++ b/abis/IAaveLendPoolAddressesProvider.json @@ -0,0 +1,15 @@ +[ + { + "inputs": [], + "name": "getLendingPool", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/abis/ILendPool.json b/abis/ILendPool.json new file mode 100644 index 0000000..c6cee8d --- /dev/null +++ b/abis/ILendPool.json @@ -0,0 +1,201 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "reserveAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "internalType": "uint16", + "name": "referralCode", + "type": "uint16" + } + ], + "name": "borrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + } + ], + "name": "getNftAuctionData", + "outputs": [ + { + "internalType": "uint256", + "name": "loanId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "bidderAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "bidPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bidBorrowAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bidFine", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + } + ], + "name": "getNftDebtData", + "outputs": [ + { + "internalType": "uint256", + "name": "loanId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "reserveAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "totalCollateral", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalDebt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "availableBorrows", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "healthFactor", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bidFine", + "type": "uint256" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "repay", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/abis/ILendPoolAddressesProvider.json b/abis/ILendPoolAddressesProvider.json new file mode 100644 index 0000000..5c08127 --- /dev/null +++ b/abis/ILendPoolAddressesProvider.json @@ -0,0 +1,28 @@ +[ + { + "inputs": [], + "name": "getLendPool", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLendPoolLoan", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/abis/ILendPoolLoan.json b/abis/ILendPoolLoan.json new file mode 100644 index 0000000..b952299 --- /dev/null +++ b/abis/ILendPoolLoan.json @@ -0,0 +1,45 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "loanId", + "type": "uint256" + } + ], + "name": "borrowerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nftAsset", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nftTokenId", + "type": "uint256" + } + ], + "name": "getCollateralLoanId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/abis/LendingMigrator.json b/abis/LendingMigrator.json new file mode 100644 index 0000000..66fb0f4 --- /dev/null +++ b/abis/LendingMigrator.json @@ -0,0 +1,300 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "aaveAddressesProvider", + "outputs": [ + { + "internalType": "contract IAaveLendPoolAddressesProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "aaveLendPool", + "outputs": [ + { + "internalType": "contract IAaveLendPool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bakc", + "outputs": [ + { + "internalType": "contract IERC721Upgradeable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bayc", + "outputs": [ + { + "internalType": "contract IERC721Upgradeable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bendAddressesProvider", + "outputs": [ + { + "internalType": "contract ILendPoolAddressesProvider", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bendLendLoan", + "outputs": [ + { + "internalType": "contract ILendPoolLoan", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bendLendPool", + "outputs": [ + { + "internalType": "contract ILendPool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "assets", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "premiums", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "executeOperation", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "aaveAddressesProvider_", + "type": "address" + }, + { + "internalType": "address", + "name": "bendAddressesProvider_", + "type": "address" + }, + { + "internalType": "contract INftPool", + "name": "nftPool_", + "type": "address" + }, + { + "internalType": "contract IStakedNft", + "name": "stBayc_", + "type": "address" + }, + { + "internalType": "contract IStakedNft", + "name": "stMayc_", + "type": "address" + }, + { + "internalType": "contract IStakedNft", + "name": "stBakc_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "mayc", + "outputs": [ + { + "internalType": "contract IERC721Upgradeable", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nftPool", + "outputs": [ + { + "internalType": "contract INftPool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stBakc", + "outputs": [ + { + "internalType": "contract IStakedNft", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stBayc", + "outputs": [ + { + "internalType": "contract IStakedNft", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stMayc", + "outputs": [ + { + "internalType": "contract IStakedNft", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/contracts/misc/LendingMigrator.sol b/contracts/misc/LendingMigrator.sol new file mode 100644 index 0000000..91340dc --- /dev/null +++ b/contracts/misc/LendingMigrator.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +import {IAaveLendPoolAddressesProvider} from "./interfaces/IAaveLendPoolAddressesProvider.sol"; +import {IAaveLendPool} from "./interfaces/IAaveLendPool.sol"; +import {IAaveFlashLoanReceiver} from "./interfaces/IAaveFlashLoanReceiver.sol"; +import {ILendPoolAddressesProvider} from "./interfaces/ILendPoolAddressesProvider.sol"; +import {ILendPool} from "./interfaces/ILendPool.sol"; +import {ILendPoolLoan} from "./interfaces/ILendPoolLoan.sol"; + +import {IStakedNft} from "../interfaces/IStakedNft.sol"; +import {INftPool} from "../interfaces/INftPool.sol"; + +contract LendingMigrator is IAaveFlashLoanReceiver, ReentrancyGuardUpgradeable, OwnableUpgradeable { + IAaveLendPoolAddressesProvider public aaveAddressesProvider; + IAaveLendPool public aaveLendPool; + ILendPoolAddressesProvider public bendAddressesProvider; + ILendPool public bendLendPool; + ILendPoolLoan public bendLendLoan; + + INftPool public nftPool; + IStakedNft public stBayc; + IStakedNft public stMayc; + IStakedNft public stBakc; + + IERC721Upgradeable public bayc; + IERC721Upgradeable public mayc; + IERC721Upgradeable public bakc; + + function initialize( + address aaveAddressesProvider_, + address bendAddressesProvider_, + address nftPool_, + address stBayc_, + address stMayc_, + address stBakc_ + ) external initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + + nftPool = INftPool(nftPool_); + stBayc = IStakedNft(stBayc_); + stMayc = IStakedNft(stMayc_); + stBakc = IStakedNft(stBakc_); + + bayc = IERC721Upgradeable(stBayc.underlyingAsset()); + mayc = IERC721Upgradeable(stMayc.underlyingAsset()); + bakc = IERC721Upgradeable(stBakc.underlyingAsset()); + + aaveAddressesProvider = IAaveLendPoolAddressesProvider(aaveAddressesProvider_); + aaveLendPool = IAaveLendPool(aaveAddressesProvider.getLendingPool()); + + bendAddressesProvider = ILendPoolAddressesProvider(bendAddressesProvider_); + bendLendPool = ILendPool(bendAddressesProvider.getLendPool()); + bendLendLoan = ILendPoolLoan(bendAddressesProvider.getLendPoolLoan()); + + IERC721Upgradeable(bayc).setApprovalForAll(address(nftPool), true); + IERC721Upgradeable(mayc).setApprovalForAll(address(nftPool), true); + IERC721Upgradeable(bakc).setApprovalForAll(address(nftPool), true); + } + + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address, /*initiator*/ + bytes calldata params + ) external returns (bool) { + require(msg.sender == address(aaveLendPool), "Migrator: caller must be aave lending pool"); + require( + assets.length == 1 && amounts.length == 1 && premiums.length == 1, + "Migrator: multiple assets not supported" + ); + + (address[] memory nftAssets, uint256[] memory nftTokenIds) = abi.decode(params, (address[], uint256[])); + require(nftAssets.length == nftTokenIds.length, "Migrator: inconsistent assets and token ids"); + + uint256 aaveFlashLoanFeeRatio = aaveLendPool.FLASHLOAN_PREMIUM_TOTAL(); + + IERC20Upgradeable(assets[0]).approve(address(bendLendPool), type(uint256).max); + + for (uint256 i = 0; i < nftTokenIds.length; i++) { + RepayAndBorrowLocaVars memory vars; + vars.nftAsset = nftAssets[i]; + vars.nftTokenId = nftTokenIds[i]; + vars.flashLoanAsset = assets[0]; + vars.flashLoanFeeRatio = aaveFlashLoanFeeRatio; + + _repayAndBorrowPerNft(vars); + } + + IERC20Upgradeable(assets[0]).approve(address(bendLendPool), 0); + + IERC20Upgradeable(assets[0]).approve(msg.sender, (amounts[0] + premiums[0])); + + return true; + } + + struct RepayAndBorrowLocaVars { + address nftAsset; + uint256 nftTokenId; + address flashLoanAsset; + uint256 flashLoanFeeRatio; + uint256 loanId; + address borrower; + address debtReserve; + uint256 debtTotalAmount; + uint256 debtRemainAmount; + uint256 redeemAmount; + uint256 bidFine; + uint256 debtTotalAmountWithBidFine; + uint256 balanceBeforeRepay; + uint256[] nftTokenIds; + uint256 flashLoanPremium; + uint256 debtBorrowAmountWithFee; + uint256 balanceBeforeBorrow; + uint256 balanceAfterBorrow; + } + + function _repayAndBorrowPerNft(RepayAndBorrowLocaVars memory vars) internal { + (vars.loanId, , , , vars.bidFine) = bendLendPool.getNftAuctionData(vars.nftAsset, vars.nftTokenId); + (, vars.debtReserve, , vars.debtTotalAmount, , ) = bendLendPool.getNftDebtData(vars.nftAsset, vars.nftTokenId); + vars.debtTotalAmountWithBidFine = vars.debtTotalAmount + vars.bidFine; + + vars.borrower = bendLendLoan.borrowerOf(vars.loanId); + vars.balanceBeforeRepay = IERC20Upgradeable(vars.debtReserve).balanceOf(address(this)); + + require(vars.debtReserve == vars.flashLoanAsset, "Migrator: invalid flash loan asset"); + require(vars.debtTotalAmountWithBidFine <= vars.balanceBeforeRepay, "Migrator: insufficent to repay debt"); + + // redeem first if nft is in auction + if (vars.bidFine > 0) { + vars.redeemAmount = (vars.debtTotalAmount * 2) / 3; + bendLendPool.redeem(vars.nftAsset, vars.nftTokenId, vars.redeemAmount, vars.bidFine); + + (, , , vars.debtRemainAmount, , ) = bendLendPool.getNftDebtData(vars.nftAsset, vars.nftTokenId); + } else { + vars.debtRemainAmount = vars.debtTotalAmount; + } + + // repay all the old debt + bendLendPool.repay(vars.nftAsset, vars.nftTokenId, vars.debtRemainAmount); + + // stake original nft to the staking pool + IERC721Upgradeable(vars.nftAsset).safeTransferFrom(vars.borrower, address(address(this)), vars.nftTokenId); + vars.nftTokenIds = new uint256[](1); + vars.nftTokenIds[0] = vars.nftTokenId; + nftPool.deposit(vars.nftAsset, vars.nftTokenIds); + + // borrow new debt with the staked nft + vars.balanceBeforeBorrow = IERC20Upgradeable(vars.debtReserve).balanceOf(address(this)); + + IStakedNft stNftAsset = getStakedNFTAsset(vars.nftAsset); + IERC721Upgradeable(address(stNftAsset)).approve(address(bendLendPool), vars.nftTokenId); + + vars.flashLoanPremium = (vars.debtTotalAmountWithBidFine * vars.flashLoanFeeRatio) / 10000; + vars.debtBorrowAmountWithFee = vars.debtTotalAmountWithBidFine + vars.flashLoanPremium; + bendLendPool.borrow( + vars.debtReserve, + vars.debtBorrowAmountWithFee, + address(stNftAsset), + vars.nftTokenId, + vars.borrower, + 0 + ); + + vars.balanceAfterBorrow = IERC20Upgradeable(vars.debtReserve).balanceOf(address(this)); + require(vars.balanceAfterBorrow == (vars.balanceBeforeBorrow + vars.debtBorrowAmountWithFee)); + } + + function getStakedNFTAsset(address nftAsset) internal view returns (IStakedNft) { + if (nftAsset == address(bayc)) { + return stBayc; + } else if (nftAsset == address(mayc)) { + return stMayc; + } else if (nftAsset == address(bakc)) { + return stBakc; + } else { + revert("Migrator: invalid nft asset"); + } + } + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/contracts/misc/interfaces/IAaveFlashLoanReceiver.sol b/contracts/misc/interfaces/IAaveFlashLoanReceiver.sol new file mode 100644 index 0000000..9f8514e --- /dev/null +++ b/contracts/misc/interfaces/IAaveFlashLoanReceiver.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +/** + * @title IAaveFlashLoanReceiver interface + * @notice Interface for the Aave fee IFlashLoanReceiver. + * @author Bend + * @dev implement this interface to develop a flashloan-compatible flashLoanReceiver contract + **/ +interface IAaveFlashLoanReceiver { + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external returns (bool); +} diff --git a/contracts/misc/interfaces/IAaveLendPool.sol b/contracts/misc/interfaces/IAaveLendPool.sol new file mode 100644 index 0000000..50b0c87 --- /dev/null +++ b/contracts/misc/interfaces/IAaveLendPool.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +interface IAaveLendPool { + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint256); + + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata modes, + address onBehalfOf, + bytes calldata params, + uint16 referralCode + ) external; +} diff --git a/contracts/misc/interfaces/IAaveLendPoolAddressesProvider.sol b/contracts/misc/interfaces/IAaveLendPoolAddressesProvider.sol new file mode 100644 index 0000000..df20440 --- /dev/null +++ b/contracts/misc/interfaces/IAaveLendPoolAddressesProvider.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +/** + * @title IAaveLendPoolAddressesProvider contract + * @dev Main registry of addresses part of or connected to the aave protocol, including permissioned roles + * - Acting also as factory of proxies and admin of those, so with right to change its implementations + * - Owned by the Aave Governance + * @author Bend + **/ +interface IAaveLendPoolAddressesProvider { + function getLendingPool() external view returns (address); +} diff --git a/contracts/misc/interfaces/ILendPool.sol b/contracts/misc/interfaces/ILendPool.sol new file mode 100644 index 0000000..b640050 --- /dev/null +++ b/contracts/misc/interfaces/ILendPool.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +interface ILendPool { + function borrow( + address reserveAsset, + uint256 amount, + address nftAsset, + uint256 nftTokenId, + address onBehalfOf, + uint16 referralCode + ) external; + + function repay( + address nftAsset, + uint256 nftTokenId, + uint256 amount + ) external returns (uint256, bool); + + function redeem( + address nftAsset, + uint256 nftTokenId, + uint256 amount, + uint256 bidFine + ) external returns (uint256); + + function getNftDebtData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address reserveAsset, + uint256 totalCollateral, + uint256 totalDebt, + uint256 availableBorrows, + uint256 healthFactor + ); + + function getNftAuctionData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address bidderAddress, + uint256 bidPrice, + uint256 bidBorrowAmount, + uint256 bidFine + ); +} diff --git a/contracts/misc/interfaces/ILendPoolAddressesProvider.sol b/contracts/misc/interfaces/ILendPoolAddressesProvider.sol new file mode 100644 index 0000000..78fd5fe --- /dev/null +++ b/contracts/misc/interfaces/ILendPoolAddressesProvider.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +/** + * @title LendPoolAddressesProvider contract + * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles + * - Acting also as factory of proxies and admin of those, so with right to change its implementations + * - Owned by the Bend Governance + * @author Bend + **/ +interface ILendPoolAddressesProvider { + function getLendPool() external view returns (address); + + function getLendPoolLoan() external view returns (address); +} diff --git a/contracts/misc/interfaces/ILendPoolLoan.sol b/contracts/misc/interfaces/ILendPoolLoan.sol new file mode 100644 index 0000000..ac3f306 --- /dev/null +++ b/contracts/misc/interfaces/ILendPoolLoan.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +interface ILendPoolLoan { + function getCollateralLoanId(address nftAsset, uint256 nftTokenId) external view returns (uint256); + + function borrowerOf(uint256 loanId) external view returns (address); +} diff --git a/contracts/test/MockAaveLendPool.sol b/contracts/test/MockAaveLendPool.sol new file mode 100644 index 0000000..f4f52e1 --- /dev/null +++ b/contracts/test/MockAaveLendPool.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IAaveLendPool} from "../misc/interfaces/IAaveLendPool.sol"; +import {IAaveFlashLoanReceiver} from "../misc/interfaces/IAaveFlashLoanReceiver.sol"; + +contract MockAaveLendPool is IAaveLendPool { + uint256 private _flashLoanPremiumTotal; + + constructor() { + _flashLoanPremiumTotal = 9; + } + + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint256) { + return _flashLoanPremiumTotal; + } + + struct FlashLoanLocalVars { + IAaveFlashLoanReceiver receiver; + uint256 i; + address currentAsset; + uint256 currentAmount; + uint256 currentPremium; + uint256 currentAmountPlusPremium; + } + + function flashLoan( + address receiverAddress, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata, /*modes*/ + address, /*onBehalfOf*/ + bytes calldata params, + uint16 /*referralCode*/ + ) external { + FlashLoanLocalVars memory vars; + vars.receiver = IAaveFlashLoanReceiver(receiverAddress); + + uint256[] memory premiums = new uint256[](assets.length); + + for (vars.i = 0; vars.i < assets.length; vars.i++) { + premiums[vars.i] = (amounts[vars.i] * _flashLoanPremiumTotal) / 10000; + + IERC20(assets[vars.i]).transfer(receiverAddress, amounts[vars.i]); + } + + require( + vars.receiver.executeOperation(assets, amounts, premiums, msg.sender, params), + "AaveLendPool: Flashloan execution failed" + ); + + for (vars.i = 0; vars.i < assets.length; vars.i++) { + vars.currentAsset = assets[vars.i]; + vars.currentAmount = amounts[vars.i]; + vars.currentPremium = premiums[vars.i]; + vars.currentAmountPlusPremium = vars.currentAmount + vars.currentPremium; + + IERC20(vars.currentAsset).transferFrom(receiverAddress, vars.currentAsset, vars.currentAmountPlusPremium); + } + } +} diff --git a/contracts/test/MockAaveLendPoolAddressesProvider.sol b/contracts/test/MockAaveLendPoolAddressesProvider.sol new file mode 100644 index 0000000..a584b87 --- /dev/null +++ b/contracts/test/MockAaveLendPoolAddressesProvider.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {IAaveLendPoolAddressesProvider} from "../misc/interfaces/IAaveLendPoolAddressesProvider.sol"; + +contract MocKAaveLendPoolAddressesProvider is IAaveLendPoolAddressesProvider { + address public lendingPool; + + function setLendingPool(address lendingPool_) public { + lendingPool = lendingPool_; + } + + function getLendingPool() public view returns (address) { + return lendingPool; + } +} diff --git a/contracts/test/MockBendLendPool.sol b/contracts/test/MockBendLendPool.sol new file mode 100644 index 0000000..446fcaa --- /dev/null +++ b/contracts/test/MockBendLendPool.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import {ILendPoolAddressesProvider} from "../misc/interfaces/ILendPoolAddressesProvider.sol"; +import {ILendPool} from "../misc/interfaces/ILendPool.sol"; + +import "./MockBendLendPoolLoan.sol"; + +contract MockBendLendPool is ILendPool { + ILendPoolAddressesProvider public addressesProvider; + + function setAddressesProvider(address addressesProvider_) public { + addressesProvider = ILendPoolAddressesProvider(addressesProvider_); + } + + function borrow( + address reserveAsset, + uint256 amount, + address nftAsset, + uint256 nftTokenId, + address, /*onBehalfOf*/ + uint16 /*referralCode*/ + ) external { + MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( + ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() + ); + lendPoolLoan.setLoanData(nftAsset, nftTokenId, msg.sender, reserveAsset, amount); + + IERC721(nftAsset).transferFrom(msg.sender, address(this), nftTokenId); + IERC20(reserveAsset).transfer(msg.sender, amount); + } + + function repay( + address nftAsset, + uint256 nftTokenId, + uint256 amount + ) external returns (uint256, bool) { + MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( + ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() + ); + + uint256 loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); + + (address borrower, address reserveAsset, uint256 totalDebt, uint256 bidFineInLoan) = lendPoolLoan.getLoanData( + loanId + ); + require(bidFineInLoan == 0, "loan is in auction"); + + if (amount > totalDebt) { + amount = totalDebt; + } + totalDebt -= amount; + lendPoolLoan.setTotalDebt(loanId, totalDebt); + + IERC20(reserveAsset).transferFrom(msg.sender, address(this), amount); + + if (totalDebt == 0) { + IERC721(nftAsset).transferFrom(address(this), borrower, nftTokenId); + } + + return (amount, true); + } + + function redeem( + address nftAsset, + uint256 nftTokenId, + uint256 amount, + uint256 bidFine + ) external returns (uint256) { + MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( + ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() + ); + uint256 loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); + + (, address reserveAsset, uint256 totalDebt, uint256 bidFineInLoan) = lendPoolLoan.getLoanData(loanId); + uint256 maxRedeemAmount = (totalDebt * 9) / 10; + require(amount <= maxRedeemAmount, "exceed max redeem amount"); + require(bidFine == bidFineInLoan, "insufficient bid fine"); + + IERC20(reserveAsset).transferFrom(msg.sender, address(this), (amount + bidFine)); + + totalDebt -= amount; + lendPoolLoan.setTotalDebt(loanId, totalDebt); + lendPoolLoan.setBidFine(loanId, 0); + + return amount; + } + + function getNftDebtData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address reserveAsset, + uint256 totalCollateral, + uint256 totalDebt, + uint256 availableBorrows, + uint256 healthFactor + ) + { + MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( + ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() + ); + + loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); + (, reserveAsset, totalDebt, ) = lendPoolLoan.getLoanData(loanId); + + totalCollateral = totalDebt; + availableBorrows = 0; + healthFactor = 1e18; + } + + function getNftAuctionData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address bidderAddress, + uint256 bidPrice, + uint256 bidBorrowAmount, + uint256 bidFine + ) + { + MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( + ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() + ); + + loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); + + (, , bidBorrowAmount, bidFine) = lendPoolLoan.getLoanData(loanId); + + bidderAddress = msg.sender; + bidPrice = (bidBorrowAmount * 105) / 100; + } +} diff --git a/contracts/test/MockBendLendPoolAddressesProvider.sol b/contracts/test/MockBendLendPoolAddressesProvider.sol new file mode 100644 index 0000000..c7c889f --- /dev/null +++ b/contracts/test/MockBendLendPoolAddressesProvider.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {ILendPoolAddressesProvider} from "../misc/interfaces/ILendPoolAddressesProvider.sol"; + +contract MockBendLendPoolAddressesProvider is ILendPoolAddressesProvider { + address public lendPool; + address public lendPoolLoan; + + function setLendPool(address lendPool_) public { + lendPool = lendPool_; + } + + function setLendPoolLoan(address lendPoolLoan_) public { + lendPoolLoan = lendPoolLoan_; + } + + function getLendPool() public view returns (address) { + return lendPool; + } + + function getLendPoolLoan() public view returns (address) { + return lendPoolLoan; + } +} diff --git a/contracts/test/MockBendLendPoolLoan.sol b/contracts/test/MockBendLendPoolLoan.sol new file mode 100644 index 0000000..8ecb650 --- /dev/null +++ b/contracts/test/MockBendLendPoolLoan.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.18; + +import {ILendPoolLoan} from "../misc/interfaces/ILendPoolLoan.sol"; + +contract MockBendLendPoolLoan is ILendPoolLoan { + struct LoanData { + address borrower; + address reserveAsset; + uint256 totalDebt; + uint256 bidFine; + } + + mapping(address => mapping(uint256 => uint256)) private _loanIds; + mapping(uint256 => LoanData) private _loanDatas; + uint256 private _loanId; + + function setLoanData( + address nftAsset, + uint256 nftTokenId, + address borrower, + address reserveAsset, + uint256 totalDebt + ) public { + uint256 loanId = _loanIds[nftAsset][nftTokenId]; + if (loanId == 0) { + loanId = ++_loanId; + _loanIds[nftAsset][nftTokenId] = loanId; + _loanDatas[loanId] = LoanData(borrower, reserveAsset, totalDebt, 0); + } else { + _loanDatas[loanId] = LoanData(borrower, reserveAsset, totalDebt, 0); + } + } + + function setBidFine(uint256 loanId, uint256 bidFine) public { + _loanDatas[loanId].bidFine = bidFine; + } + + function setTotalDebt(uint256 loanId, uint256 totalDebt) public { + _loanDatas[loanId].totalDebt = totalDebt; + } + + function deleteLoanData(address nftAsset, uint256 nftTokenId) public { + uint256 loanId = _loanIds[nftAsset][nftTokenId]; + delete _loanIds[nftAsset][nftTokenId]; + delete _loanDatas[loanId]; + } + + function getLoanData(uint256 loanId) + public + view + returns ( + address borrower, + address reserveAsset, + uint256 totalDebt, + uint256 bidFine + ) + { + LoanData memory loanData = _loanDatas[loanId]; + borrower = loanData.borrower; + reserveAsset = loanData.reserveAsset; + totalDebt = loanData.totalDebt; + bidFine = loanData.bidFine; + } + + function getCollateralLoanId(address nftAsset, uint256 nftTokenId) public view returns (uint256) { + return _loanIds[nftAsset][nftTokenId]; + } + + function borrowerOf(uint256 loanId) public view returns (address) { + return _loanDatas[loanId].borrower; + } +} diff --git a/foundry.toml b/foundry.toml index fd6c455..6d17765 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,4 +4,10 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' -gas_reports = ["BendCoinPool", "BendNftPool", "BendStakeManager", "NftVault", "StBAYC"] \ No newline at end of file +gas_reports = ["BendCoinPool", "BendNftPool", "BendStakeManager", "NftVault", "StBAYC", "LendingMigrator"] + +[fmt] +line_length = 120 + +[fuzz] +runs = 32 \ No newline at end of file diff --git a/test/foundry/LendingMigrator.t.sol b/test/foundry/LendingMigrator.t.sol new file mode 100644 index 0000000..eb82bdb --- /dev/null +++ b/test/foundry/LendingMigrator.t.sol @@ -0,0 +1,233 @@ +pragma solidity 0.8.18; + +import "../../contracts/test/MocKAaveLendPoolAddressesProvider.sol"; +import "../../contracts/test/MockAaveLendPool.sol"; +import "../../contracts/test/MockBendLendPoolAddressesProvider.sol"; +import "../../contracts/test/MockBendLendPool.sol"; +import "../../contracts/test/MockBendLendPoolLoan.sol"; + +import "../../contracts/misc/LendingMigrator.sol"; + +import "./SetupHelper.sol"; + +contract LendingMigratorTest is SetupHelper { + MocKAaveLendPoolAddressesProvider mockAaveLendPoolAddressesProvider; + MockAaveLendPool mockAaveLendPool; + MockBendLendPoolAddressesProvider mockBendLendPoolAddressesProvider; + MockBendLendPool mockBendLendPool; + MockBendLendPoolLoan mockBendLendPoolLoan; + + LendingMigrator lendingMigrator; + + struct TestLocalVars { + uint256 i; + uint256 j; + uint256 nftCount; + // bend lending vars + address[] nftAssets; + uint256[] nftTokenIds; + uint256[] borrowAmounts; + uint256[] bidFines; + // aave flash loan vars + address[] assets; + uint256[] amounts; + uint256[] modes; + bytes params; + // results + uint256 nftLoanId; + address nftOwner; + address nftBorrower; + } + + function setUp() public override { + super.setUp(); + + mockAaveLendPoolAddressesProvider = new MocKAaveLendPoolAddressesProvider(); + mockAaveLendPool = new MockAaveLendPool(); + mockAaveLendPoolAddressesProvider.setLendingPool(address(mockAaveLendPool)); + + mockBendLendPoolAddressesProvider = new MockBendLendPoolAddressesProvider(); + mockBendLendPool = new MockBendLendPool(); + mockBendLendPoolLoan = new MockBendLendPoolLoan(); + mockBendLendPoolAddressesProvider.setLendPool(address(mockBendLendPool)); + mockBendLendPoolAddressesProvider.setLendPoolLoan(address(mockBendLendPoolLoan)); + mockBendLendPool.setAddressesProvider(address(mockBendLendPoolAddressesProvider)); + + uint256 wethAmountAave = 1000000 * 10**18; + mockWETH.mint(wethAmountAave); + mockWETH.transfer(address(mockAaveLendPool), wethAmountAave); + + uint256 usdtAmountAave = 1000000 * 10**6; + mockUSDT.mint(usdtAmountAave); + mockUSDT.transfer(address(mockAaveLendPool), usdtAmountAave); + + uint256 wethAmountBend = 1000000 * 10**18; + mockWETH.mint(wethAmountBend); + mockWETH.transfer(address(mockBendLendPool), wethAmountBend); + + uint256 usdtAmountBend = 1000000 * 10**6; + mockUSDT.mint(usdtAmountBend); + mockUSDT.transfer(address(mockBendLendPool), usdtAmountBend); + + lendingMigrator = new LendingMigrator(); + lendingMigrator.initialize( + address(mockAaveLendPoolAddressesProvider), + address(mockBendLendPoolAddressesProvider), + address(nftPool), + address(stBAYC), + address(stMAYC), + address(stBAKC) + ); + } + + function initTestLocalVars(TestLocalVars memory vars, uint256 nftCount) private pure { + vars.nftCount = nftCount; + vars.nftAssets = new address[](vars.nftCount); + vars.nftTokenIds = new uint256[](vars.nftCount); + vars.borrowAmounts = new uint256[](vars.nftCount); + vars.bidFines = new uint256[](vars.nftCount); + + vars.assets = new address[](1); + vars.amounts = new uint256[](1); + vars.modes = new uint256[](1); + } + + function testMultipleNftWithoutAuction() public { + TestLocalVars memory vars; + address testUser = testUsers[0]; + + initTestLocalVars(vars, 3); + + vm.startPrank(testUser); + + mockWETH.approve(address(mockBendLendPool), type(uint256).max); + mockBAYC.setApprovalForAll(address(lendingMigrator), true); + mockBAYC.setApprovalForAll(address(mockBendLendPool), true); + + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.nftAssets[vars.i] = address(mockBAYC); + vars.nftTokenIds[vars.i] = 100 + vars.i; + + // mint & borrow + mockBAYC.mint(vars.nftTokenIds[vars.i]); + + vars.borrowAmounts[vars.i] = 1000 * 10**18 + (vars.i + 1); + mockBendLendPool.borrow( + address(mockWETH), + vars.borrowAmounts[vars.i], + address(mockBAYC), + vars.nftTokenIds[vars.i], + testUser, + 0 + ); + } + + // flash loan + vars.assets[0] = address(mockWETH); + vars.modes[0] = 0; + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.amounts[0] = vars.borrowAmounts[vars.i]; + } + + vars.params = abi.encode(vars.nftAssets, vars.nftTokenIds); + + mockAaveLendPool.flashLoan( + address(lendingMigrator), + vars.assets, + vars.amounts, + vars.modes, + testUser, + vars.params, + 0 + ); + + // check results + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.nftLoanId = mockBendLendPoolLoan.getCollateralLoanId(vars.nftAssets[vars.i], vars.nftTokenIds[vars.i]); + + vars.nftOwner = mockBAYC.ownerOf(vars.nftTokenIds[vars.i]); + assertEq(vars.nftOwner, address(nftVault), "owner of original nft should be nft vault"); + + vars.nftOwner = stBAYC.ownerOf(vars.nftTokenIds[vars.i]); + assertEq(vars.nftOwner, address(mockBendLendPool), "owner of staked nft should be bend pool"); + + vars.nftBorrower = mockBendLendPoolLoan.borrowerOf(vars.nftLoanId); + assertEq(vars.nftBorrower, address(testUser), "borrower of loan should be test user"); + } + + vm.stopPrank(); + } + + function testMultipleNftWithAuction() public { + TestLocalVars memory vars; + address testUser = testUsers[0]; + + initTestLocalVars(vars, 3); + + vm.startPrank(testUser); + + mockWETH.approve(address(mockBendLendPool), type(uint256).max); + mockBAYC.setApprovalForAll(address(lendingMigrator), true); + mockBAYC.setApprovalForAll(address(mockBendLendPool), true); + + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.nftAssets[vars.i] = address(mockBAYC); + vars.nftTokenIds[vars.i] = 100 + vars.i; + + // mint & borrow + mockBAYC.mint(vars.nftTokenIds[vars.i]); + + vars.borrowAmounts[vars.i] = 1000 * 10**18 + (vars.i + 1); + mockBendLendPool.borrow( + address(mockWETH), + vars.borrowAmounts[vars.i], + address(mockBAYC), + vars.nftTokenIds[vars.i], + testUser, + 0 + ); + } + + // set auction + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.nftLoanId = mockBendLendPoolLoan.getCollateralLoanId(vars.nftAssets[vars.i], vars.nftTokenIds[vars.i]); + vars.bidFines[vars.i] = (vars.borrowAmounts[vars.i] * 5) / 100; + mockBendLendPoolLoan.setBidFine(vars.nftLoanId, vars.bidFines[vars.i]); + } + + // flash loan + vars.assets[0] = address(mockWETH); + vars.modes[0] = 0; + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.amounts[0] = vars.borrowAmounts[vars.i] + vars.bidFines[vars.i]; + } + + vars.params = abi.encode(vars.nftAssets, vars.nftTokenIds); + + mockAaveLendPool.flashLoan( + address(lendingMigrator), + vars.assets, + vars.amounts, + vars.modes, + testUser, + vars.params, + 0 + ); + + // check results + for (vars.i = 0; vars.i < vars.nftCount; vars.i++) { + vars.nftLoanId = mockBendLendPoolLoan.getCollateralLoanId(vars.nftAssets[vars.i], vars.nftTokenIds[vars.i]); + + vars.nftOwner = mockBAYC.ownerOf(vars.nftTokenIds[vars.i]); + assertEq(vars.nftOwner, address(nftVault), "owner of original nft should be nft vault"); + + vars.nftOwner = stBAYC.ownerOf(vars.nftTokenIds[vars.i]); + assertEq(vars.nftOwner, address(mockBendLendPool), "owner of staked nft should be bend pool"); + + vars.nftBorrower = mockBendLendPoolLoan.borrowerOf(vars.nftLoanId); + assertEq(vars.nftBorrower, address(testUser), "borrower of loan should be test user"); + } + + vm.stopPrank(); + } +} diff --git a/test/foundry/SetupHelper.sol b/test/foundry/SetupHelper.sol index 49fd45e..e5c4462 100644 --- a/test/foundry/SetupHelper.sol +++ b/test/foundry/SetupHelper.sol @@ -46,6 +46,8 @@ abstract contract SetupHelper is Test { address payable botAdmin; // mocked contracts + MintableERC20 internal mockWETH; + MintableERC20 internal mockUSDT; MintableERC20 internal mockApeCoin; MintableERC721 internal mockBAYC; MintableERC721 internal mockMAYC; @@ -80,6 +82,8 @@ abstract contract SetupHelper is Test { mockDelegationRegistry = new DelegationRegistry(); // mocked ERC20 and NFTs + mockWETH = new MintableERC20("WETH", "WETH", 18); + mockUSDT = new MintableERC20("USDT", "USDT", 6); mockApeCoin = new MintableERC20("ApeCoin", "APE", 18); mockBAYC = new MintableERC721("Mock BAYC", "BAYC");