From 5454f5d7f1d0a01842ae74628f1d0889c50fac0e Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 9 Nov 2021 01:00:51 +1100 Subject: [PATCH 001/107] .editorconfig --- .editorconfig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6a5bb19f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +trim_trailing_whitespace = true + +[*.{js,ts}] +insert_final_newline = true +indent_size = 2 +indent_style = space + +[*.sol] +insert_final_newline = true +indent_size = 4 +indent_style = space From 830e8e08cd0567034772109068724622e98bde7d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 9 Nov 2021 03:57:16 +1100 Subject: [PATCH 002/107] (Personal formatting settings) --- .prettierrc.js | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.prettierrc.js b/.prettierrc.js index b40ef700..e8e309d0 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,23 +1,23 @@ -module.exports = { - overrides: [ - { - files: "*.ts", - options: { - printWidth: 145, - semi: true, - trailingComma: "es5", - }, - }, - { - files: "*.sol", - options: { - printWidth: 140, - tabWidth: 4, - useTabs: false, - singleQuote: false, - bracketSpacing: false, - explicitTypes: "always", - }, - }, - ], -}; +module.exports = { + overrides: [ + { + files: "*.ts", + options: { + printWidth: 80, + semi: true, + trailingComma: "es5", + }, + }, + { + files: "*.sol", + options: { + printWidth: 80, + tabWidth: 4, + useTabs: false, + singleQuote: false, + bracketSpacing: false, + explicitTypes: "always", + }, + }, + ], +}; From 098e0fa7fb3b229cbc7673b48f2344b7ceeb5205 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 27 Nov 2021 04:36:58 +1100 Subject: [PATCH 003/107] Add Private Lending pool (WIP, untested) Kashi pair with the following changes: - Charge protocol fee on liquidations - Charge protocol fee on borrow open - Allow only one lender, and a whitelist of borrowers - Collect fees as soon as feasible; do not "reinvest" - Add option to seize collateral on liquidation. Bonus and fees are taken out in the collateral as well - Add expiration (liquidation-free period) --- contracts/PrivatePool.sol | 995 ++++++++++++++++++++++++ contracts/interfaces/ISimpleSwapper.sol | 18 + 2 files changed, 1013 insertions(+) create mode 100644 contracts/PrivatePool.sol create mode 100644 contracts/interfaces/ISimpleSwapper.sol diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol new file mode 100644 index 00000000..25da1b1a --- /dev/null +++ b/contracts/PrivatePool.sol @@ -0,0 +1,995 @@ +// SPDX-License-Identifier: UNLICENSED + +// Cauldron + +// ( ( ( +// )\ ) ( )\ )\ ) ( +// (((_) ( /( ))\ ((_)(()/( )( ( ( +// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ ) +// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/( +// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \)) +// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_| + +// Copyright (c) 2021 BoringCrypto - All rights reserved +// Twitter: @Boring_Crypto + +// Special thanks to: +// @0xKeno - for all his invaluable contributions +// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./interfaces/IOracle.sol"; +import "./interfaces/ISimpleSwapper.sol"; + +/// @title PrivatePool +/// @dev This contract allows contract calls to any contract (except BentoBox) +/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. +contract PrivatePool is BoringOwnable, IMasterContract { + using BoringMath for uint256; + using BoringMath128 for uint128; + using RebaseLibrary for Rebase; + using BoringERC20 for IERC20; + + event LogExchangeRate(uint256 rate); + event LogAccrue(uint256 accruedAmount, uint256 feeAmount); + event LogAddCollateral( + address indexed from, + address indexed to, + uint256 share + ); + event LogAddAsset(address indexed from, uint256 share); + event LogRemoveCollateral( + address indexed from, + address indexed to, + uint256 share + ); + event LogRemoveAsset(address indexed to, uint256 share); + event LogBorrow( + address indexed from, + address indexed to, + uint256 amount, + uint256 openFeeAmount, + uint256 part + ); + event LogRepay( + address indexed from, + address indexed to, + uint256 amount, + uint256 part + ); + event LogSeizeCollateral( + address indexed from, + uint256 collateralShare, + uint256 debtAmount, + uint256 debtPart + ); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees( + address indexed feeTo, + uint256 assetFeeShare, + uint256 collateralFeeShare + ); + + // Immutables (for MasterContract and all clones) + IBentoBoxV1 public immutable bentoBox; + PrivatePool public immutable masterContract; + + // MasterContract variables + address public feeTo; + + // Per clone variables + // Clone init settings + IERC20 public collateral; + IERC20 public asset; + IOracle public oracle; + bytes public oracleData; + + // A note on terminology: + // "Shares" are BentoBox shares. + // "Parts" and represent shares held in the debt pool + + // The BentoBox balance is the sum of the below two. + // Since that fits in a single uint128, we can often forgo overflow checks. + struct AssetBalance { + uint128 reservesShare; + uint128 feesEarnedShare; + } + AssetBalance public assetBalance; + uint256 feesOwedAmount; // Positive only if reservesShare = 0 + + // The BentoBox balance is the sum of the below two. + // Seized collateral goes to the "userCollateralShare" account of the + // lender. + struct CollateralBalance { + uint128 userTotalShare; + uint128 feesEarnedShare; + } + CollateralBalance public collateralBalance; + mapping(address => uint256) public userCollateralShare; + + // Elastic: Exact asset token amount that currently needs to be repaid + // Base: Total parts of the debt held by borrowers (borrowerDebtPart) + Rebase public totalDebt; + mapping(address => uint256) public borrowerDebtPart; + + address public lender; + mapping(address => bool) public approvedBorrowers; + + /// @notice Exchange and interest rate tracking. + /// This is 'cached' here because calls to Oracles can be very expensive. + uint256 public exchangeRate; + + struct AccrueInfo { + uint64 lastAccrued; + uint64 INTEREST_PER_SECOND; // (in units of 1/10^18) + uint64 NO_LIQUIDATIONS_BEFORE; + uint16 COLLATERALIZATION_RATE_BPS; + uint16 LIQUIDATION_MULTIPLIER_BPS; + uint16 BORROW_OPENING_FEE_BPS; + bool LIQUIDATION_SEIZE_COLLATERAL; + } + AccrueInfo public accrueInfo; + + uint256 private constant PROTOCOL_FEE_BPS = 1000; // 10% + uint256 private constant BASIS_POINTS = 10_000; + + // Must be well over BASIS_POINTS due to optimization in math: + uint256 private constant EXCHANGE_RATE_PRECISION = 1e18; + + /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`. + constructor(IBentoBoxV1 bentoBox_) public { + bentoBox = bentoBox_; + masterContract = this; + } + + struct InitSettings { + IERC20 collateral; + IERC20 asset; + IOracle oracle; + bytes oracleData; + address lender; + address[] borrowers; + uint64 INTEREST_PER_SECOND; + uint64 NO_LIQUIDATIONS_BEFORE; + uint16 COLLATERALIZATION_RATE_BPS; + uint16 LIQUIDATION_MULTIPLIER_BPS; + uint16 BORROW_OPENING_FEE_BPS; + bool LIQUIDATION_SEIZE_COLLATERAL; + } + + /// @notice Serves as the constructor for clones, as clones can't have a regular constructor + function init(bytes calldata data) public payable override { + require( + address(collateral) == address(0), + "PrivatePool: already initialized" + ); + + InitSettings memory settings = abi.decode(data, (InitSettings)); + require( + address(settings.collateral) != address(0), + "PrivatePool: bad pair" + ); + + collateral = settings.collateral; + asset = settings.asset; + oracle = settings.oracle; + oracleData = settings.oracleData; + lender = settings.lender; + + AccrueInfo memory _aI; + _aI.INTEREST_PER_SECOND = settings.INTEREST_PER_SECOND; + _aI.NO_LIQUIDATIONS_BEFORE = settings.NO_LIQUIDATIONS_BEFORE; + _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS; + _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS; + _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS; + _aI.LIQUIDATION_SEIZE_COLLATERAL = settings + .LIQUIDATION_SEIZE_COLLATERAL; + accrueInfo = _aI; + + for (uint256 i = 0; i < settings.borrowers.length; i++) { + approvedBorrowers[settings.borrowers[i]] = true; + } + } + + /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees. + function accrue() public { + AccrueInfo memory _accrueInfo = accrueInfo; + // Number of seconds since accrue was called + uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued; + if (elapsedTime == 0) { + return; + } + accrueInfo.lastAccrued = uint64(block.timestamp); + + Rebase memory _totalDebt = totalDebt; + if (_totalDebt.base == 0) { + return; + } + uint256 extraAmount = uint256(_totalDebt.elastic) + .mul(_accrueInfo.INTEREST_PER_SECOND) + .mul(elapsedTime) / 1e18; + _totalDebt.elastic = _totalDebt.elastic.add(extraAmount.to128()); + totalDebt = _totalDebt; + + uint256 feeAmount = extraAmount.mul(PROTOCOL_FEE_BPS) / BASIS_POINTS; + + AssetBalance memory _assetBalance = assetBalance; + if (_assetBalance.reservesShare == 0) { + // Fees owed are always part of the debt, and the debt just got + // at least `feeAmount` added to it. If that fit, so does this: + feesOwedAmount += feeAmount; + } else { + uint256 feeShare = bentoBox.toShare(asset, feeAmount, false); + if (_assetBalance.reservesShare < feeShare) { + _assetBalance.feesEarnedShare += _assetBalance.reservesShare; + feesOwedAmount += bentoBox.toAmount( + asset, + feeShare - _assetBalance.reservesShare, + false + ); + _assetBalance.reservesShare = 0; + } else { + // feesEarned + fee <= feesEarned + reserves <= Bento balance: + _assetBalance.reservesShare -= uint128(feeShare); + _assetBalance.feesEarnedShare += uint128(feeShare); + } + assetBalance = _assetBalance; + } + + emit LogAccrue(extraAmount, feeAmount); + } + + /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`. + /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls. + function _isSolvent(address borrower, uint256 _exchangeRate) + internal + view + returns (bool) + { + // accrue must have already been called! + uint256 debtPart = borrowerDebtPart[borrower]; + if (debtPart == 0) return true; + uint256 collateralShare = userCollateralShare[borrower]; + if (collateralShare == 0) return false; + + Rebase memory _totalDebt = totalDebt; + + return + bentoBox.toAmount( + collateral, + collateralShare.mul(EXCHANGE_RATE_PRECISION / BASIS_POINTS).mul( + accrueInfo.COLLATERALIZATION_RATE_BPS + ), + false + ) >= + // Moved exchangeRate here instead of dividing the other side to + // preserve more precision + debtPart.mul(_totalDebt.elastic).mul(_exchangeRate) / + _totalDebt.base; + } + + /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body. + modifier solvent() { + _; + require( + _isSolvent(msg.sender, exchangeRate), + "PrivatePool: borrower insolvent" + ); + } + + /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset. + /// This function is supposed to be invoked if needed because Oracle queries can be expensive. + /// @return updated True if `exchangeRate` was updated. + /// @return rate The new exchange rate. + function updateExchangeRate() public returns (bool updated, uint256 rate) { + (updated, rate) = oracle.get(oracleData); + + if (updated) { + exchangeRate = rate; + emit LogExchangeRate(rate); + } else { + // Return the old rate if fetching wasn't successful + rate = exchangeRate; + } + } + + /// @dev Helper function to move tokens. + /// @param token The ERC-20 token. + /// @param share The amount in shares to add. + /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True. + /// Only used for accounting checks. + /// @param skim If True, only does a balance check on this contract. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + function _addTokens( + IERC20 token, + uint256 share, + uint256 total, + bool skim + ) internal { + if (skim) { + require( + share <= bentoBox.balanceOf(token, address(this)).sub(total), + "PrivatePool: Skim too much" + ); + } else { + bentoBox.transfer(token, msg.sender, address(this), share); + } + } + + /// @notice Adds `collateral` from msg.sender to the account `to`. + /// @param to The receiver of the tokens. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param share The amount of shares to add for `to`. + function addCollateral( + address to, + bool skim, + uint256 share + ) public { + uint256 supplied = userCollateralShare[to]; + require(supplied > 0 || approvedBorrowers[to], "Unapproved borrower"); + + userCollateralShare[to] = supplied + share; + CollateralBalance memory _collateralBalance = collateralBalance; + // No over/underflow: it fits in the BentoBox total + uint256 prevTotal = _collateralBalance.userTotalShare + + _collateralBalance.feesEarnedShare; + collateralBalance.userTotalShare = + _collateralBalance.userTotalShare + + uint128(share); + _addTokens(collateral, share, prevTotal, skim); + emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share); + } + + /// @dev Concrete implementation of `removeCollateral`. + function _removeCollateral(address to, uint256 share) internal { + userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub( + share + ); + collateralBalance.userTotalShare -= uint128(share); + emit LogRemoveCollateral(msg.sender, to, share); + bentoBox.transfer(collateral, address(this), to, share); + } + + /// @notice Removes `share` amount of collateral and transfers it to `to`. + /// @param to The receiver of the shares. + /// @param share Amount of shares to remove. + function removeCollateral(address to, uint256 share) public solvent { + // accrue must be called because we check solvency + accrue(); + _removeCollateral(to, share); + } + + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// @param toReservesShare Amount of shares to reserves. + /// @param toReservesAmount Token amount. Ignored if `toReservesShare` nonzero. + /// @param toFeesAmount Token fee amount. Ignored if `toReservesShare` nonzero. + function _receiveAsset( + bool skim, + uint256 toReservesShare, + // (There is no case where we pass along a fee in shares) + uint256 toReservesAmount, + uint256 toFeesAmount + ) internal { + IERC20 _asset = asset; + AssetBalance memory _assetBalance = assetBalance; + uint256 priorAssetTotalShare = _assetBalance.reservesShare + + _assetBalance.feesEarnedShare; + Rebase memory bentoBoxTotals = bentoBox.totals(_asset); + + uint256 toFeesShare = 0; + if (toReservesShare == 0) { + toReservesShare = bentoBoxTotals.toBase(toReservesAmount, true); + if (toFeesAmount > 0) { + toFeesShare = bentoBoxTotals.toBase(toFeesAmount, false); + } + } + uint256 takenShare = toReservesShare.add(toFeesShare); + + if (_assetBalance.reservesShare == 0) { + uint256 _feesOwedAmount = feesOwedAmount; + if (_feesOwedAmount > 0) { + uint256 feesOwedShare = bentoBoxTotals.toBase( + _feesOwedAmount, + false + ); + // New fees cannot pay off existing fees: + if (toReservesShare < feesOwedShare) { + feesOwedAmount = bentoBoxTotals.toElastic( + feesOwedShare - toReservesShare, + false + ); + _assetBalance.feesEarnedShare += uint128(takenShare); + } else { + feesOwedAmount = 0; + // No overflow: assuming the transfer at the end succeeds: + // feesOwedShare <= toReservesShare <= (Bento balance), + _assetBalance.feesEarnedShare += uint128( + feesOwedShare + toFeesShare + ); + _assetBalance.reservesShare = uint128( + toReservesShare - feesOwedShare + ); + } + } else { + _assetBalance.reservesShare = uint128(toReservesShare); + _assetBalance.feesEarnedShare += uint128(toFeesShare); + } + } else { + _assetBalance.reservesShare += uint128(toReservesShare); + _assetBalance.feesEarnedShare += uint128(toFeesShare); + } + assetBalance = _assetBalance; + + _addTokens(_asset, takenShare, priorAssetTotalShare, skim); + } + + /// @dev Concrete implementation of `addAsset`. + function _addAsset(bool skim, uint256 share) internal { + _receiveAsset(skim, share, 0, 0); + emit LogAddAsset(skim ? address(bentoBox) : msg.sender, share); + } + + /// @notice Adds assets to the lending pair. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param share The amount of shares to add. + function addAsset(bool skim, uint256 share) public { + accrue(); + _addAsset(skim, share); + } + + /// @dev Concrete implementation of `removeAsset`. + function _removeAsset(address to, uint256 share) internal { + require(msg.sender == lender, "Not the lender"); + // Fits in a uint128 if the transfer goes through: + assetBalance.reservesShare = assetBalance.reservesShare.sub( + uint128(share) + ); + bentoBox.transfer(asset, address(this), to, share); + emit LogRemoveAsset(to, share); + } + + /// @notice Removes an asset from msg.sender and transfers it to `to`. + /// @param to The address that receives the removed assets. + /// @param share The amount of shares to remove. + function removeAsset(address to, uint256 share) public { + accrue(); + _removeAsset(to, share); + } + + /// @dev Concrete implementation of `borrow`. + function _borrow(address to, uint256 amount) + internal + returns (uint256 part, uint256 share) + { + require(approvedBorrowers[msg.sender], "Unapproved borrower"); + IERC20 _asset = asset; + Rebase memory bentoBoxTotals = bentoBox.totals(_asset); + AccrueInfo memory _accrueInfo = accrueInfo; + + share = bentoBoxTotals.toBase(amount, false); + + uint256 openFeeAmount = amount.mul(_accrueInfo.BORROW_OPENING_FEE_BPS) / + BASIS_POINTS; + uint256 protocolFeeAmount = openFeeAmount.mul(PROTOCOL_FEE_BPS) / + BASIS_POINTS; + uint256 protocolFeeShare = bentoBoxTotals.toBase( + protocolFeeAmount, + false + ); + + // The protocol component of the opening fee cannot be owed: + AssetBalance memory _assetBalance = assetBalance; + _assetBalance.reservesShare = _assetBalance.reservesShare.sub( + (share.add(protocolFeeShare)).to128() + ); + // No overflow if the above succeeded: + // feesEarned + protocolFee <= feesEarned + reserves <= Bento balance + _assetBalance.feesEarnedShare += uint128(protocolFeeShare); + assetBalance = _assetBalance; + + (totalDebt, part) = totalDebt.add(amount.add(openFeeAmount), true); + borrowerDebtPart[msg.sender] = borrowerDebtPart[msg.sender].add(part); + emit LogBorrow(msg.sender, to, amount, openFeeAmount, part); + + bentoBox.transfer(_asset, address(this), to, share); + } + + /// @notice Sender borrows `amount` and transfers it to `to`. + /// @return part Total part of the debt held by borrowers. + /// @return share Total amount in shares borrowed. + function borrow(address to, uint256 amount) + public + solvent + returns (uint256 part, uint256 share) + { + accrue(); + (part, share) = _borrow(to, amount); + } + + /// @dev Concrete implementation of `repay`. + function _repay( + address to, + bool skim, + uint256 part + ) internal returns (uint256 amount) { + (totalDebt, amount) = totalDebt.sub(part, true); + borrowerDebtPart[to] = borrowerDebtPart[to].sub(part); + _receiveAsset(skim, 0, amount, 0); + emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part); + } + + /// @notice Repays a loan. + /// @param to Address of the borrower this payment should go. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param part The amount to repay as part. See `borrowerDebtPart`. + /// @return amount The total amount repayed. + function repay( + address to, + bool skim, + uint256 part + ) public returns (uint256 amount) { + accrue(); + amount = _repay(to, skim, part); + } + + // Functions that need accrue to be called + uint8 internal constant ACTION_ADD_ASSET = 1; + uint8 internal constant ACTION_REPAY = 2; + uint8 internal constant ACTION_REMOVE_ASSET = 3; + uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; + uint8 internal constant ACTION_BORROW = 5; + uint8 internal constant ACTION_GET_REPAY_SHARE = 6; + uint8 internal constant ACTION_GET_REPAY_PART = 7; + uint8 internal constant ACTION_ACCRUE = 8; + + // Functions that don't need accrue to be called + uint8 internal constant ACTION_ADD_COLLATERAL = 10; + uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11; + + // Function on BentoBox + uint8 internal constant ACTION_BENTO_DEPOSIT = 20; + uint8 internal constant ACTION_BENTO_WITHDRAW = 21; + uint8 internal constant ACTION_BENTO_TRANSFER = 22; + uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; + uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; + + // Any external call (except to BentoBox) + uint8 internal constant ACTION_CALL = 30; + + int256 internal constant USE_VALUE1 = -1; + int256 internal constant USE_VALUE2 = -2; + + /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. + function _num( + int256 inNum, + uint256 value1, + uint256 value2 + ) internal pure returns (uint256 outNum) { + outNum = inNum >= 0 + ? uint256(inNum) + : (inNum == USE_VALUE1 ? value1 : value2); + } + + /// @dev Helper function for depositing into `bentoBox`. + function _bentoDeposit( + bytes memory data, + uint256 value, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode( + data, + (IERC20, address, int256, int256) + ); + amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors + share = int256(_num(share, value1, value2)); + return + bentoBox.deposit{value: value}( + token, + msg.sender, + to, + uint256(amount), + uint256(share) + ); + } + + /// @dev Helper function to withdraw from the `bentoBox`. + function _bentoWithdraw( + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode( + data, + (IERC20, address, int256, int256) + ); + return + bentoBox.withdraw( + token, + msg.sender, + to, + _num(amount, value1, value2), + _num(share, value1, value2) + ); + } + + /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. + /// Calls to `bentoBox` are not allowed for obvious security reasons. + /// This also means that calls made from this contract shall *not* be trusted. + function _call( + uint256 value, + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (bytes memory, uint8) { + ( + address callee, + bytes memory callData, + bool useValue1, + bool useValue2, + uint8 returnValues + ) = abi.decode(data, (address, bytes, bool, bool, uint8)); + + if (useValue1 && !useValue2) { + callData = abi.encodePacked(callData, value1); + } else if (!useValue1 && useValue2) { + callData = abi.encodePacked(callData, value2); + } else if (useValue1 && useValue2) { + callData = abi.encodePacked(callData, value1, value2); + } + + require( + callee != address(bentoBox) && callee != address(this), + "PrivatePool: can't call" + ); + + (bool success, bytes memory returnData) = callee.call{value: value}( + callData + ); + require(success, "PrivatePool: call failed"); + return (returnData, returnValues); + } + + struct CookStatus { + bool needsSolvencyCheck; + bool hasAccrued; + } + + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + CookStatus memory status; + for (uint256 i = 0; i < actions.length; i++) { + uint8 action = actions[i]; + if (!status.hasAccrued && action < 10) { + accrue(); + status.hasAccrued = true; + } + if (action == ACTION_ADD_COLLATERAL) { + (int256 share, address to, bool skim) = abi.decode( + datas[i], + (int256, address, bool) + ); + addCollateral(to, skim, _num(share, value1, value2)); + } else if (action == ACTION_ADD_ASSET) { + (int256 share, bool skim) = abi.decode( + datas[i], + (int256, bool) + ); + _addAsset(skim, _num(share, value1, value2)); + } else if (action == ACTION_REPAY) { + (int256 part, address to, bool skim) = abi.decode( + datas[i], + (int256, address, bool) + ); + _repay(to, skim, _num(part, value1, value2)); + } else if (action == ACTION_REMOVE_ASSET) { + (int256 share, address to) = abi.decode( + datas[i], + (int256, address) + ); + _removeAsset(to, _num(share, value1, value2)); + } else if (action == ACTION_REMOVE_COLLATERAL) { + (int256 share, address to) = abi.decode( + datas[i], + (int256, address) + ); + _removeCollateral(to, _num(share, value1, value2)); + status.needsSolvencyCheck = true; + } else if (action == ACTION_BORROW) { + (int256 amount, address to) = abi.decode( + datas[i], + (int256, address) + ); + (value1, value2) = _borrow(to, _num(amount, value1, value2)); + status.needsSolvencyCheck = true; + } else if (action == ACTION_UPDATE_EXCHANGE_RATE) { + (bool must_update, uint256 minRate, uint256 maxRate) = abi + .decode(datas[i], (bool, uint256, uint256)); + (bool updated, uint256 rate) = updateExchangeRate(); + require( + (!must_update || updated) && + rate > minRate && + (maxRate == 0 || rate > maxRate), + "PrivatePool: rate not ok" + ); + } else if (action == ACTION_BENTO_SETAPPROVAL) { + ( + address user, + address _masterContract, + bool approved, + uint8 v, + bytes32 r, + bytes32 s + ) = abi.decode( + datas[i], + (address, address, bool, uint8, bytes32, bytes32) + ); + bentoBox.setMasterContractApproval( + user, + _masterContract, + approved, + v, + r, + s + ); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit( + datas[i], + values[i], + value1, + value2 + ); + } else if (action == ACTION_BENTO_WITHDRAW) { + (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + } else if (action == ACTION_BENTO_TRANSFER) { + (IERC20 token, address to, int256 share) = abi.decode( + datas[i], + (IERC20, address, int256) + ); + bentoBox.transfer( + token, + msg.sender, + to, + _num(share, value1, value2) + ); + } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { + ( + IERC20 token, + address[] memory tos, + uint256[] memory shares + ) = abi.decode(datas[i], (IERC20, address[], uint256[])); + bentoBox.transferMultiple(token, msg.sender, tos, shares); + } else if (action == ACTION_CALL) { + (bytes memory returnData, uint8 returnValues) = _call( + values[i], + datas[i], + value1, + value2 + ); + + if (returnValues == 1) { + (value1) = abi.decode(returnData, (uint256)); + } else if (returnValues == 2) { + (value1, value2) = abi.decode( + returnData, + (uint256, uint256) + ); + } + } else if (action == ACTION_GET_REPAY_SHARE) { + int256 part = abi.decode(datas[i], (int256)); + value1 = bentoBox.toShare( + asset, + totalDebt.toElastic(_num(part, value1, value2), true), + true + ); + } else if (action == ACTION_GET_REPAY_PART) { + int256 amount = abi.decode(datas[i], (int256)); + value1 = totalDebt.toBase(_num(amount, value1, value2), false); + } + } + + if (status.needsSolvencyCheck) { + require( + _isSolvent(msg.sender, exchangeRate), + "PrivatePool: borrower insolvent" + ); + } + } + + /// @notice Handles the liquidation of borrowers' balances, once the borrowers' amount of collateral is too low. + /// @param borrowers An array of borrower addresses. + /// @param maxDebtParts A one-to-one mapping to `borrowers`, contains maximum part (not token amount) of the debt that will be liquidated of the respective borrower. + /// @param to Address of the receiver if `swapper` is zero. + /// @param swapper Contract address of the `ISimpleSwapper` implementation. + function liquidate( + address[] calldata borrowers, + uint256[] calldata maxDebtParts, + address to, + ISimpleSwapper swapper + ) public { + // Oracle can fail but we still need to allow liquidations + (, uint256 _exchangeRate) = updateExchangeRate(); + accrue(); + + AccrueInfo memory _accrueInfo = accrueInfo; + require( + block.timestamp >= _accrueInfo.NO_LIQUIDATIONS_BEFORE, + "Non-liquidation period" + ); + + uint256 allCollateralShare; + uint256 allDebtAmount; + uint256 allDebtPart; + Rebase memory _totalDebt = totalDebt; + Rebase memory bentoBoxTotals = bentoBox.totals(collateral); + for (uint256 i = 0; i < borrowers.length; i++) { + address borrower = borrowers[i]; + if (!_isSolvent(borrower, _exchangeRate)) { + uint256 debtPart; + { + uint256 availableDebtPart = borrowerDebtPart[borrower]; + debtPart = maxDebtParts[i] > availableDebtPart + ? availableDebtPart + : maxDebtParts[i]; + borrowerDebtPart[borrower] = availableDebtPart.sub( + debtPart + ); + } + uint256 debtAmount = _totalDebt.toElastic(debtPart, false); + uint256 collateralShare = bentoBoxTotals.toBase( + debtAmount.mul(_accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul( + _exchangeRate + ) / (BASIS_POINTS * EXCHANGE_RATE_PRECISION), + false + ); + + userCollateralShare[borrower] = userCollateralShare[borrower] + .sub(collateralShare); + + if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) { + emit LogSeizeCollateral( + borrower, + collateralShare, + debtAmount, + debtPart + ); + } else { + emit LogRemoveCollateral( + borrower, + swapper == ISimpleSwapper(0) ? to : address(swapper), + collateralShare + ); + emit LogRepay( + swapper == ISimpleSwapper(0) + ? msg.sender + : address(swapper), + borrower, + debtAmount, + debtPart + ); + } + + // No overflow: subtracting from user balances succeeded + allCollateralShare = allCollateralShare.add(collateralShare); + allDebtAmount = allDebtAmount.add(debtAmount); + allDebtPart = allDebtPart.add(debtPart); + } + } + require(allDebtAmount != 0, "PrivatePool: all are solvent"); + _totalDebt.elastic = _totalDebt.elastic.sub(allDebtAmount.to128()); + _totalDebt.base = _totalDebt.base.sub(allDebtPart.to128()); + totalDebt = _totalDebt; + + if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) { + // As with normal liquidations, the liquidator gets the excess, the + // protocol gets a cut of the excess, and the lender gets 100% of + // the value of the loan. + // Math: All collateral fits in 128 bits (BentoBox), so the + // multiplications are safe: + uint256 excessShare = (allCollateralShare * + (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BASIS_POINTS)) / + BASIS_POINTS; + uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BASIS_POINTS; + uint256 lenderShare = allCollateralShare - excessShare; + // (Stack depth): liquidatorShare = excessShare - feeShare; + + { + CollateralBalance memory _collateralBalance = collateralBalance; + // No underflow: All amounts fit in the collateral BentoBox total + _collateralBalance.userTotalShare -= uint128(excessShare); + _collateralBalance.feesEarnedShare += uint128(feeShare); + collateralBalance = _collateralBalance; + } + userCollateralShare[lender] += lenderShare; + bentoBox.transfer( + collateral, + address(this), + to, + excessShare - feeShare + ); + } else { + // No underflow: summands fit in user balances + collateralBalance.userTotalShare -= uint128(allCollateralShare); + + // Charge the protocol fee over the excess. + uint256 feeAmount = (allDebtAmount.mul( + _accrueInfo.LIQUIDATION_MULTIPLIER_BPS + ) / BASIS_POINTS).sub(allDebtAmount).mul(PROTOCOL_FEE_BPS) / + BASIS_POINTS; // Distribution Amount + + // Swap using a swapper freely chosen by the caller + // Open (flash) liquidation: get proceeds first and provide the + // borrow after + bentoBox.transfer( + collateral, + address(this), + swapper == ISimpleSwapper(0) ? to : address(swapper), + allCollateralShare + ); + if (swapper != ISimpleSwapper(0)) { + // TODO: Somehow split _receiveAsset to reduce loads? + IERC20 _asset = asset; + swapper.swap( + collateral, + _asset, + msg.sender, + bentoBox.toShare( + _asset, + allDebtAmount.add(feeAmount), + true + ), + allCollateralShare + ); + } + _receiveAsset(false, 0, allDebtAmount, feeAmount); + } + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + accrue(); + address to = masterContract.feeTo(); + + uint256 assetShare = assetBalance.feesEarnedShare; + if (assetShare > 0) { + bentoBox.transfer(asset, address(this), to, assetShare); + assetBalance.feesEarnedShare = 0; + } + + uint256 collateralShare = collateralBalance.feesEarnedShare; + if (collateralShare > 0) { + bentoBox.transfer(collateral, address(this), to, collateralShare); + collateralBalance.feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, assetShare, collateralShare); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } +} diff --git a/contracts/interfaces/ISimpleSwapper.sol b/contracts/interfaces/ISimpleSwapper.sol new file mode 100644 index 00000000..0f32f6ff --- /dev/null +++ b/contracts/interfaces/ISimpleSwapper.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >= 0.6.12; +import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; + +interface ISimpleSwapper { + /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper. + /// Swaps it for at least 'amountToMin' of token 'to'. + /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer. + /// Returns the amount of tokens 'to' transferred to BentoBox. + /// (The BentoBox skim function will be used by the caller to get the swapped funds). + function swap( + IERC20 fromToken, + IERC20 toToken, + address recipient, + uint256 shareToMin, + uint256 shareFrom + ) external returns (uint256 extraShare, uint256 shareReturned); +} From ae063b346f6b32ea09a4f1fe40154f64e9ccf7d6 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 27 Nov 2021 04:36:58 +1100 Subject: [PATCH 004/107] (Archive oracles and swappers to silence warnings) --- {contracts => archive/contracts}/oracles/3CrvOracle.sol | 0 {contracts => archive/contracts}/oracles/3CryptoOracle.sol | 0 {contracts => archive/contracts}/oracles/AGLDUniV3Oracle.sol | 0 {contracts => archive/contracts}/oracles/ALCXOracle.sol | 0 {contracts => archive/contracts}/oracles/AVAXOracle.sol | 0 {contracts => archive/contracts}/oracles/AvaxLPOracle.sol | 0 {contracts => archive/contracts}/oracles/AvaxUsdtOracle.sol | 0 {contracts => archive/contracts}/oracles/BNBOracle.sol | 0 {contracts => archive/contracts}/oracles/BandOracleFTM.sol | 0 {contracts => archive/contracts}/oracles/CakeOracle.sol | 0 {contracts => archive/contracts}/oracles/ChainlinkOracle.sol | 0 {contracts => archive/contracts}/oracles/CompositeOracle.sol | 0 {contracts => archive/contracts}/oracles/CompoundOracle.sol | 0 {contracts => archive/contracts}/oracles/LPChainlinkOracle.sol | 0 {contracts => archive/contracts}/oracles/MimAvaxOracle.sol | 0 {contracts => archive/contracts}/oracles/PeggedOracle.sol | 0 {contracts => archive/contracts}/oracles/ProxyOracle.sol | 0 {contracts => archive/contracts}/oracles/RenBTCCrvOracle.sol | 0 {contracts => archive/contracts}/oracles/SimpleSLPTWAP0Oracle.sol | 0 {contracts => archive/contracts}/oracles/SimpleSLPTWAP1Oracle.sol | 0 {contracts => archive/contracts}/oracles/SpellOracle.sol | 0 {contracts => archive/contracts}/oracles/SpellTWAPOracle.sol | 0 {contracts => archive/contracts}/oracles/USTOracle.sol | 0 {contracts => archive/contracts}/oracles/YVCrvStETHOracle.sol | 0 {contracts => archive/contracts}/oracles/YVIronBankOracle.sol | 0 {contracts => archive/contracts}/oracles/YearnChainlinkOracle.sol | 0 {contracts => archive/contracts}/oracles/dQuickOracle.sol | 0 {contracts => archive/contracts}/oracles/sSpellOracle.sol | 0 {contracts => archive/contracts}/oracles/wMEMOOracle.sol | 0 {contracts => archive/contracts}/oracles/wOHMLinkOracle.sol | 0 {contracts => archive/contracts}/oracles/xJOEOracle.sol | 0 {contracts => archive/contracts}/oracles/xSUSHIOracle.sol | 0 .../contracts}/swappers/Leverage/AGLDLevSwapper.sol | 0 .../contracts}/swappers/Leverage/ALCXLevSwapper.sol | 0 .../contracts}/swappers/Leverage/ArbEthLevSwapper.sol | 0 .../contracts}/swappers/Leverage/AvaxUsdtLevSwapper.sol | 0 .../contracts}/swappers/Leverage/FTMLevSwapper.sol | 0 .../contracts}/swappers/Leverage/MimAvaxLevSwapper.sol | 0 .../contracts}/swappers/Leverage/RenCrvLevSwapper.sol | 0 .../contracts}/swappers/Leverage/SpellLevSwapper.sol | 0 .../contracts}/swappers/Leverage/ThreeCryptoLevSwapper.sol | 0 .../contracts}/swappers/Leverage/USTLevSwapper.sol | 0 .../contracts}/swappers/Leverage/UsdcAvaxLevSwapper.sol | 0 .../contracts}/swappers/Leverage/XJoeLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVCrvStETHLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVIBLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVUSDCLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVUSDTLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVWETHLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVXSushiLevSwapper.sol | 0 .../contracts}/swappers/Leverage/YVYFILevSwapper.sol | 0 .../contracts}/swappers/Leverage/wMemoLevSwapper.sol | 0 .../contracts}/swappers/Leverage/wOHMLevSwapper.sol | 0 .../contracts}/swappers/Liquidations/AGLDSwapper.sol | 0 .../contracts}/swappers/Liquidations/ALCXSwapper.sol | 0 .../contracts}/swappers/Liquidations/ArbEthSwapper.sol | 0 .../contracts}/swappers/Liquidations/AvaxUsdtSwapper.sol | 0 .../contracts}/swappers/Liquidations/FTMSwapper.sol | 0 .../contracts}/swappers/Liquidations/MimAvaxSwapper.sol | 0 .../contracts}/swappers/Liquidations/RenCrvSwapper.sol | 0 .../contracts}/swappers/Liquidations/SpellSuperSwapper.sol | 0 .../contracts}/swappers/Liquidations/ThreeCryptoSwapper.sol | 0 .../contracts}/swappers/Liquidations/USTSwapper.sol | 0 .../contracts}/swappers/Liquidations/UsdcAvaxSwapper.sol | 0 .../contracts}/swappers/Liquidations/XJoeSwapper.sol | 0 .../contracts}/swappers/Liquidations/XSushiSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVCrvStETHSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVIBSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVUSDCSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVUSDTSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVWETHSwapper.sol | 0 .../contracts}/swappers/Liquidations/YVYFISwapper.sol | 0 .../contracts}/swappers/Liquidations/sSpellSwapper.sol | 0 .../contracts}/swappers/Liquidations/wMEMOSwapper.sol | 0 .../contracts}/swappers/Liquidations/wOHMSwapper.sol | 0 {contracts => archive/contracts}/swappers/SpellSwapper.sol | 0 .../contracts}/swappers/SushiSwapMultiSwapper.sol | 0 {contracts => archive/contracts}/swappers/SushiSwapSwapper.sol | 0 78 files changed, 0 insertions(+), 0 deletions(-) rename {contracts => archive/contracts}/oracles/3CrvOracle.sol (100%) rename {contracts => archive/contracts}/oracles/3CryptoOracle.sol (100%) rename {contracts => archive/contracts}/oracles/AGLDUniV3Oracle.sol (100%) rename {contracts => archive/contracts}/oracles/ALCXOracle.sol (100%) rename {contracts => archive/contracts}/oracles/AVAXOracle.sol (100%) rename {contracts => archive/contracts}/oracles/AvaxLPOracle.sol (100%) rename {contracts => archive/contracts}/oracles/AvaxUsdtOracle.sol (100%) rename {contracts => archive/contracts}/oracles/BNBOracle.sol (100%) rename {contracts => archive/contracts}/oracles/BandOracleFTM.sol (100%) rename {contracts => archive/contracts}/oracles/CakeOracle.sol (100%) rename {contracts => archive/contracts}/oracles/ChainlinkOracle.sol (100%) rename {contracts => archive/contracts}/oracles/CompositeOracle.sol (100%) rename {contracts => archive/contracts}/oracles/CompoundOracle.sol (100%) rename {contracts => archive/contracts}/oracles/LPChainlinkOracle.sol (100%) rename {contracts => archive/contracts}/oracles/MimAvaxOracle.sol (100%) rename {contracts => archive/contracts}/oracles/PeggedOracle.sol (100%) rename {contracts => archive/contracts}/oracles/ProxyOracle.sol (100%) rename {contracts => archive/contracts}/oracles/RenBTCCrvOracle.sol (100%) rename {contracts => archive/contracts}/oracles/SimpleSLPTWAP0Oracle.sol (100%) rename {contracts => archive/contracts}/oracles/SimpleSLPTWAP1Oracle.sol (100%) rename {contracts => archive/contracts}/oracles/SpellOracle.sol (100%) rename {contracts => archive/contracts}/oracles/SpellTWAPOracle.sol (100%) rename {contracts => archive/contracts}/oracles/USTOracle.sol (100%) rename {contracts => archive/contracts}/oracles/YVCrvStETHOracle.sol (100%) rename {contracts => archive/contracts}/oracles/YVIronBankOracle.sol (100%) rename {contracts => archive/contracts}/oracles/YearnChainlinkOracle.sol (100%) rename {contracts => archive/contracts}/oracles/dQuickOracle.sol (100%) rename {contracts => archive/contracts}/oracles/sSpellOracle.sol (100%) rename {contracts => archive/contracts}/oracles/wMEMOOracle.sol (100%) rename {contracts => archive/contracts}/oracles/wOHMLinkOracle.sol (100%) rename {contracts => archive/contracts}/oracles/xJOEOracle.sol (100%) rename {contracts => archive/contracts}/oracles/xSUSHIOracle.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/AGLDLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/ALCXLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/ArbEthLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/AvaxUsdtLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/FTMLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/MimAvaxLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/RenCrvLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/SpellLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/ThreeCryptoLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/USTLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/UsdcAvaxLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/XJoeLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVCrvStETHLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVIBLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVUSDCLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVUSDTLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVWETHLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVXSushiLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/YVYFILevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/wMemoLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Leverage/wOHMLevSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/AGLDSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/ALCXSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/ArbEthSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/AvaxUsdtSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/FTMSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/MimAvaxSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/RenCrvSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/SpellSuperSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/ThreeCryptoSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/USTSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/UsdcAvaxSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/XJoeSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/XSushiSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVCrvStETHSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVIBSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVUSDCSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVUSDTSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVWETHSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/YVYFISwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/sSpellSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/wMEMOSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/Liquidations/wOHMSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/SpellSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/SushiSwapMultiSwapper.sol (100%) rename {contracts => archive/contracts}/swappers/SushiSwapSwapper.sol (100%) diff --git a/contracts/oracles/3CrvOracle.sol b/archive/contracts/oracles/3CrvOracle.sol similarity index 100% rename from contracts/oracles/3CrvOracle.sol rename to archive/contracts/oracles/3CrvOracle.sol diff --git a/contracts/oracles/3CryptoOracle.sol b/archive/contracts/oracles/3CryptoOracle.sol similarity index 100% rename from contracts/oracles/3CryptoOracle.sol rename to archive/contracts/oracles/3CryptoOracle.sol diff --git a/contracts/oracles/AGLDUniV3Oracle.sol b/archive/contracts/oracles/AGLDUniV3Oracle.sol similarity index 100% rename from contracts/oracles/AGLDUniV3Oracle.sol rename to archive/contracts/oracles/AGLDUniV3Oracle.sol diff --git a/contracts/oracles/ALCXOracle.sol b/archive/contracts/oracles/ALCXOracle.sol similarity index 100% rename from contracts/oracles/ALCXOracle.sol rename to archive/contracts/oracles/ALCXOracle.sol diff --git a/contracts/oracles/AVAXOracle.sol b/archive/contracts/oracles/AVAXOracle.sol similarity index 100% rename from contracts/oracles/AVAXOracle.sol rename to archive/contracts/oracles/AVAXOracle.sol diff --git a/contracts/oracles/AvaxLPOracle.sol b/archive/contracts/oracles/AvaxLPOracle.sol similarity index 100% rename from contracts/oracles/AvaxLPOracle.sol rename to archive/contracts/oracles/AvaxLPOracle.sol diff --git a/contracts/oracles/AvaxUsdtOracle.sol b/archive/contracts/oracles/AvaxUsdtOracle.sol similarity index 100% rename from contracts/oracles/AvaxUsdtOracle.sol rename to archive/contracts/oracles/AvaxUsdtOracle.sol diff --git a/contracts/oracles/BNBOracle.sol b/archive/contracts/oracles/BNBOracle.sol similarity index 100% rename from contracts/oracles/BNBOracle.sol rename to archive/contracts/oracles/BNBOracle.sol diff --git a/contracts/oracles/BandOracleFTM.sol b/archive/contracts/oracles/BandOracleFTM.sol similarity index 100% rename from contracts/oracles/BandOracleFTM.sol rename to archive/contracts/oracles/BandOracleFTM.sol diff --git a/contracts/oracles/CakeOracle.sol b/archive/contracts/oracles/CakeOracle.sol similarity index 100% rename from contracts/oracles/CakeOracle.sol rename to archive/contracts/oracles/CakeOracle.sol diff --git a/contracts/oracles/ChainlinkOracle.sol b/archive/contracts/oracles/ChainlinkOracle.sol similarity index 100% rename from contracts/oracles/ChainlinkOracle.sol rename to archive/contracts/oracles/ChainlinkOracle.sol diff --git a/contracts/oracles/CompositeOracle.sol b/archive/contracts/oracles/CompositeOracle.sol similarity index 100% rename from contracts/oracles/CompositeOracle.sol rename to archive/contracts/oracles/CompositeOracle.sol diff --git a/contracts/oracles/CompoundOracle.sol b/archive/contracts/oracles/CompoundOracle.sol similarity index 100% rename from contracts/oracles/CompoundOracle.sol rename to archive/contracts/oracles/CompoundOracle.sol diff --git a/contracts/oracles/LPChainlinkOracle.sol b/archive/contracts/oracles/LPChainlinkOracle.sol similarity index 100% rename from contracts/oracles/LPChainlinkOracle.sol rename to archive/contracts/oracles/LPChainlinkOracle.sol diff --git a/contracts/oracles/MimAvaxOracle.sol b/archive/contracts/oracles/MimAvaxOracle.sol similarity index 100% rename from contracts/oracles/MimAvaxOracle.sol rename to archive/contracts/oracles/MimAvaxOracle.sol diff --git a/contracts/oracles/PeggedOracle.sol b/archive/contracts/oracles/PeggedOracle.sol similarity index 100% rename from contracts/oracles/PeggedOracle.sol rename to archive/contracts/oracles/PeggedOracle.sol diff --git a/contracts/oracles/ProxyOracle.sol b/archive/contracts/oracles/ProxyOracle.sol similarity index 100% rename from contracts/oracles/ProxyOracle.sol rename to archive/contracts/oracles/ProxyOracle.sol diff --git a/contracts/oracles/RenBTCCrvOracle.sol b/archive/contracts/oracles/RenBTCCrvOracle.sol similarity index 100% rename from contracts/oracles/RenBTCCrvOracle.sol rename to archive/contracts/oracles/RenBTCCrvOracle.sol diff --git a/contracts/oracles/SimpleSLPTWAP0Oracle.sol b/archive/contracts/oracles/SimpleSLPTWAP0Oracle.sol similarity index 100% rename from contracts/oracles/SimpleSLPTWAP0Oracle.sol rename to archive/contracts/oracles/SimpleSLPTWAP0Oracle.sol diff --git a/contracts/oracles/SimpleSLPTWAP1Oracle.sol b/archive/contracts/oracles/SimpleSLPTWAP1Oracle.sol similarity index 100% rename from contracts/oracles/SimpleSLPTWAP1Oracle.sol rename to archive/contracts/oracles/SimpleSLPTWAP1Oracle.sol diff --git a/contracts/oracles/SpellOracle.sol b/archive/contracts/oracles/SpellOracle.sol similarity index 100% rename from contracts/oracles/SpellOracle.sol rename to archive/contracts/oracles/SpellOracle.sol diff --git a/contracts/oracles/SpellTWAPOracle.sol b/archive/contracts/oracles/SpellTWAPOracle.sol similarity index 100% rename from contracts/oracles/SpellTWAPOracle.sol rename to archive/contracts/oracles/SpellTWAPOracle.sol diff --git a/contracts/oracles/USTOracle.sol b/archive/contracts/oracles/USTOracle.sol similarity index 100% rename from contracts/oracles/USTOracle.sol rename to archive/contracts/oracles/USTOracle.sol diff --git a/contracts/oracles/YVCrvStETHOracle.sol b/archive/contracts/oracles/YVCrvStETHOracle.sol similarity index 100% rename from contracts/oracles/YVCrvStETHOracle.sol rename to archive/contracts/oracles/YVCrvStETHOracle.sol diff --git a/contracts/oracles/YVIronBankOracle.sol b/archive/contracts/oracles/YVIronBankOracle.sol similarity index 100% rename from contracts/oracles/YVIronBankOracle.sol rename to archive/contracts/oracles/YVIronBankOracle.sol diff --git a/contracts/oracles/YearnChainlinkOracle.sol b/archive/contracts/oracles/YearnChainlinkOracle.sol similarity index 100% rename from contracts/oracles/YearnChainlinkOracle.sol rename to archive/contracts/oracles/YearnChainlinkOracle.sol diff --git a/contracts/oracles/dQuickOracle.sol b/archive/contracts/oracles/dQuickOracle.sol similarity index 100% rename from contracts/oracles/dQuickOracle.sol rename to archive/contracts/oracles/dQuickOracle.sol diff --git a/contracts/oracles/sSpellOracle.sol b/archive/contracts/oracles/sSpellOracle.sol similarity index 100% rename from contracts/oracles/sSpellOracle.sol rename to archive/contracts/oracles/sSpellOracle.sol diff --git a/contracts/oracles/wMEMOOracle.sol b/archive/contracts/oracles/wMEMOOracle.sol similarity index 100% rename from contracts/oracles/wMEMOOracle.sol rename to archive/contracts/oracles/wMEMOOracle.sol diff --git a/contracts/oracles/wOHMLinkOracle.sol b/archive/contracts/oracles/wOHMLinkOracle.sol similarity index 100% rename from contracts/oracles/wOHMLinkOracle.sol rename to archive/contracts/oracles/wOHMLinkOracle.sol diff --git a/contracts/oracles/xJOEOracle.sol b/archive/contracts/oracles/xJOEOracle.sol similarity index 100% rename from contracts/oracles/xJOEOracle.sol rename to archive/contracts/oracles/xJOEOracle.sol diff --git a/contracts/oracles/xSUSHIOracle.sol b/archive/contracts/oracles/xSUSHIOracle.sol similarity index 100% rename from contracts/oracles/xSUSHIOracle.sol rename to archive/contracts/oracles/xSUSHIOracle.sol diff --git a/contracts/swappers/Leverage/AGLDLevSwapper.sol b/archive/contracts/swappers/Leverage/AGLDLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/AGLDLevSwapper.sol rename to archive/contracts/swappers/Leverage/AGLDLevSwapper.sol diff --git a/contracts/swappers/Leverage/ALCXLevSwapper.sol b/archive/contracts/swappers/Leverage/ALCXLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/ALCXLevSwapper.sol rename to archive/contracts/swappers/Leverage/ALCXLevSwapper.sol diff --git a/contracts/swappers/Leverage/ArbEthLevSwapper.sol b/archive/contracts/swappers/Leverage/ArbEthLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/ArbEthLevSwapper.sol rename to archive/contracts/swappers/Leverage/ArbEthLevSwapper.sol diff --git a/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol b/archive/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol rename to archive/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol diff --git a/contracts/swappers/Leverage/FTMLevSwapper.sol b/archive/contracts/swappers/Leverage/FTMLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/FTMLevSwapper.sol rename to archive/contracts/swappers/Leverage/FTMLevSwapper.sol diff --git a/contracts/swappers/Leverage/MimAvaxLevSwapper.sol b/archive/contracts/swappers/Leverage/MimAvaxLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/MimAvaxLevSwapper.sol rename to archive/contracts/swappers/Leverage/MimAvaxLevSwapper.sol diff --git a/contracts/swappers/Leverage/RenCrvLevSwapper.sol b/archive/contracts/swappers/Leverage/RenCrvLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/RenCrvLevSwapper.sol rename to archive/contracts/swappers/Leverage/RenCrvLevSwapper.sol diff --git a/contracts/swappers/Leverage/SpellLevSwapper.sol b/archive/contracts/swappers/Leverage/SpellLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/SpellLevSwapper.sol rename to archive/contracts/swappers/Leverage/SpellLevSwapper.sol diff --git a/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol b/archive/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol rename to archive/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol diff --git a/contracts/swappers/Leverage/USTLevSwapper.sol b/archive/contracts/swappers/Leverage/USTLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/USTLevSwapper.sol rename to archive/contracts/swappers/Leverage/USTLevSwapper.sol diff --git a/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol b/archive/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol rename to archive/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol diff --git a/contracts/swappers/Leverage/XJoeLevSwapper.sol b/archive/contracts/swappers/Leverage/XJoeLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/XJoeLevSwapper.sol rename to archive/contracts/swappers/Leverage/XJoeLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol b/archive/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVIBLevSwapper.sol b/archive/contracts/swappers/Leverage/YVIBLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVIBLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVIBLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVUSDCLevSwapper.sol b/archive/contracts/swappers/Leverage/YVUSDCLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVUSDCLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVUSDCLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVUSDTLevSwapper.sol b/archive/contracts/swappers/Leverage/YVUSDTLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVUSDTLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVUSDTLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVWETHLevSwapper.sol b/archive/contracts/swappers/Leverage/YVWETHLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVWETHLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVWETHLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVXSushiLevSwapper.sol b/archive/contracts/swappers/Leverage/YVXSushiLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVXSushiLevSwapper.sol rename to archive/contracts/swappers/Leverage/YVXSushiLevSwapper.sol diff --git a/contracts/swappers/Leverage/YVYFILevSwapper.sol b/archive/contracts/swappers/Leverage/YVYFILevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/YVYFILevSwapper.sol rename to archive/contracts/swappers/Leverage/YVYFILevSwapper.sol diff --git a/contracts/swappers/Leverage/wMemoLevSwapper.sol b/archive/contracts/swappers/Leverage/wMemoLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/wMemoLevSwapper.sol rename to archive/contracts/swappers/Leverage/wMemoLevSwapper.sol diff --git a/contracts/swappers/Leverage/wOHMLevSwapper.sol b/archive/contracts/swappers/Leverage/wOHMLevSwapper.sol similarity index 100% rename from contracts/swappers/Leverage/wOHMLevSwapper.sol rename to archive/contracts/swappers/Leverage/wOHMLevSwapper.sol diff --git a/contracts/swappers/Liquidations/AGLDSwapper.sol b/archive/contracts/swappers/Liquidations/AGLDSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/AGLDSwapper.sol rename to archive/contracts/swappers/Liquidations/AGLDSwapper.sol diff --git a/contracts/swappers/Liquidations/ALCXSwapper.sol b/archive/contracts/swappers/Liquidations/ALCXSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/ALCXSwapper.sol rename to archive/contracts/swappers/Liquidations/ALCXSwapper.sol diff --git a/contracts/swappers/Liquidations/ArbEthSwapper.sol b/archive/contracts/swappers/Liquidations/ArbEthSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/ArbEthSwapper.sol rename to archive/contracts/swappers/Liquidations/ArbEthSwapper.sol diff --git a/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol b/archive/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/AvaxUsdtSwapper.sol rename to archive/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol diff --git a/contracts/swappers/Liquidations/FTMSwapper.sol b/archive/contracts/swappers/Liquidations/FTMSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/FTMSwapper.sol rename to archive/contracts/swappers/Liquidations/FTMSwapper.sol diff --git a/contracts/swappers/Liquidations/MimAvaxSwapper.sol b/archive/contracts/swappers/Liquidations/MimAvaxSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/MimAvaxSwapper.sol rename to archive/contracts/swappers/Liquidations/MimAvaxSwapper.sol diff --git a/contracts/swappers/Liquidations/RenCrvSwapper.sol b/archive/contracts/swappers/Liquidations/RenCrvSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/RenCrvSwapper.sol rename to archive/contracts/swappers/Liquidations/RenCrvSwapper.sol diff --git a/contracts/swappers/Liquidations/SpellSuperSwapper.sol b/archive/contracts/swappers/Liquidations/SpellSuperSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/SpellSuperSwapper.sol rename to archive/contracts/swappers/Liquidations/SpellSuperSwapper.sol diff --git a/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol b/archive/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/ThreeCryptoSwapper.sol rename to archive/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol diff --git a/contracts/swappers/Liquidations/USTSwapper.sol b/archive/contracts/swappers/Liquidations/USTSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/USTSwapper.sol rename to archive/contracts/swappers/Liquidations/USTSwapper.sol diff --git a/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol b/archive/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/UsdcAvaxSwapper.sol rename to archive/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol diff --git a/contracts/swappers/Liquidations/XJoeSwapper.sol b/archive/contracts/swappers/Liquidations/XJoeSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/XJoeSwapper.sol rename to archive/contracts/swappers/Liquidations/XJoeSwapper.sol diff --git a/contracts/swappers/Liquidations/XSushiSwapper.sol b/archive/contracts/swappers/Liquidations/XSushiSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/XSushiSwapper.sol rename to archive/contracts/swappers/Liquidations/XSushiSwapper.sol diff --git a/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol b/archive/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVCrvStETHSwapper.sol rename to archive/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol diff --git a/contracts/swappers/Liquidations/YVIBSwapper.sol b/archive/contracts/swappers/Liquidations/YVIBSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVIBSwapper.sol rename to archive/contracts/swappers/Liquidations/YVIBSwapper.sol diff --git a/contracts/swappers/Liquidations/YVUSDCSwapper.sol b/archive/contracts/swappers/Liquidations/YVUSDCSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVUSDCSwapper.sol rename to archive/contracts/swappers/Liquidations/YVUSDCSwapper.sol diff --git a/contracts/swappers/Liquidations/YVUSDTSwapper.sol b/archive/contracts/swappers/Liquidations/YVUSDTSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVUSDTSwapper.sol rename to archive/contracts/swappers/Liquidations/YVUSDTSwapper.sol diff --git a/contracts/swappers/Liquidations/YVWETHSwapper.sol b/archive/contracts/swappers/Liquidations/YVWETHSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVWETHSwapper.sol rename to archive/contracts/swappers/Liquidations/YVWETHSwapper.sol diff --git a/contracts/swappers/Liquidations/YVYFISwapper.sol b/archive/contracts/swappers/Liquidations/YVYFISwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/YVYFISwapper.sol rename to archive/contracts/swappers/Liquidations/YVYFISwapper.sol diff --git a/contracts/swappers/Liquidations/sSpellSwapper.sol b/archive/contracts/swappers/Liquidations/sSpellSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/sSpellSwapper.sol rename to archive/contracts/swappers/Liquidations/sSpellSwapper.sol diff --git a/contracts/swappers/Liquidations/wMEMOSwapper.sol b/archive/contracts/swappers/Liquidations/wMEMOSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/wMEMOSwapper.sol rename to archive/contracts/swappers/Liquidations/wMEMOSwapper.sol diff --git a/contracts/swappers/Liquidations/wOHMSwapper.sol b/archive/contracts/swappers/Liquidations/wOHMSwapper.sol similarity index 100% rename from contracts/swappers/Liquidations/wOHMSwapper.sol rename to archive/contracts/swappers/Liquidations/wOHMSwapper.sol diff --git a/contracts/swappers/SpellSwapper.sol b/archive/contracts/swappers/SpellSwapper.sol similarity index 100% rename from contracts/swappers/SpellSwapper.sol rename to archive/contracts/swappers/SpellSwapper.sol diff --git a/contracts/swappers/SushiSwapMultiSwapper.sol b/archive/contracts/swappers/SushiSwapMultiSwapper.sol similarity index 100% rename from contracts/swappers/SushiSwapMultiSwapper.sol rename to archive/contracts/swappers/SushiSwapMultiSwapper.sol diff --git a/contracts/swappers/SushiSwapSwapper.sol b/archive/contracts/swappers/SushiSwapSwapper.sol similarity index 100% rename from contracts/swappers/SushiSwapSwapper.sol rename to archive/contracts/swappers/SushiSwapSwapper.sol From 3048143e57a88491fd7f67d6d67adf4edb05879b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 4 Dec 2021 04:13:58 +1100 Subject: [PATCH 005/107] Enforce limits on init() settings --- contracts/PrivatePool.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 25da1b1a..15190377 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -176,6 +176,14 @@ contract PrivatePool is BoringOwnable, IMasterContract { address(settings.collateral) != address(0), "PrivatePool: bad pair" ); + require( + settings.LIQUIDATION_MULTIPLIER_BPS >= BASIS_POINTS, + "PrivatePool: negative liquidation bonus" + ); + require( + settings.COLLATERALIZATION_RATE_BPS <= BASIS_POINTS, + "PrivatePool: bad collateralization rate" + ); collateral = settings.collateral; asset = settings.asset; From e868f5b6919bda38e584ddf95e70ad1c4cc8dc12 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 11 Dec 2021 03:01:46 +1100 Subject: [PATCH 006/107] Make feesOwedAmount public --- contracts/PrivatePool.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 15190377..17dbfe6e 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -102,7 +102,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint128 feesEarnedShare; } AssetBalance public assetBalance; - uint256 feesOwedAmount; // Positive only if reservesShare = 0 + uint256 public feesOwedAmount; // Positive only if reservesShare = 0 // The BentoBox balance is the sum of the below two. // Seized collateral goes to the "userCollateralShare" account of the From 1365a2e771fd02c07017dd4e3643a9e9e424847e Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 10 Dec 2021 04:29:24 +1100 Subject: [PATCH 007/107] Reword some things for symmetry/uniformity --- contracts/PrivatePool.sol | 41 +++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 17dbfe6e..7ba7f8b0 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -138,9 +138,9 @@ contract PrivatePool is BoringOwnable, IMasterContract { AccrueInfo public accrueInfo; uint256 private constant PROTOCOL_FEE_BPS = 1000; // 10% - uint256 private constant BASIS_POINTS = 10_000; + uint256 private constant BPS = 10_000; - // Must be well over BASIS_POINTS due to optimization in math: + // Must be well over BPS due to optimization in math: uint256 private constant EXCHANGE_RATE_PRECISION = 1e18; /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`. @@ -177,11 +177,11 @@ contract PrivatePool is BoringOwnable, IMasterContract { "PrivatePool: bad pair" ); require( - settings.LIQUIDATION_MULTIPLIER_BPS >= BASIS_POINTS, + settings.LIQUIDATION_MULTIPLIER_BPS >= BPS, "PrivatePool: negative liquidation bonus" ); require( - settings.COLLATERALIZATION_RATE_BPS <= BASIS_POINTS, + settings.COLLATERALIZATION_RATE_BPS <= BPS, "PrivatePool: bad collateralization rate" ); @@ -226,7 +226,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { _totalDebt.elastic = _totalDebt.elastic.add(extraAmount.to128()); totalDebt = _totalDebt; - uint256 feeAmount = extraAmount.mul(PROTOCOL_FEE_BPS) / BASIS_POINTS; + uint256 feeAmount = extraAmount.mul(PROTOCOL_FEE_BPS) / BPS; AssetBalance memory _assetBalance = assetBalance; if (_assetBalance.reservesShare == 0) { @@ -272,7 +272,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { return bentoBox.toAmount( collateral, - collateralShare.mul(EXCHANGE_RATE_PRECISION / BASIS_POINTS).mul( + collateralShare.mul(EXCHANGE_RATE_PRECISION / BPS).mul( accrueInfo.COLLATERALIZATION_RATE_BPS ), false @@ -324,7 +324,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { if (skim) { require( share <= bentoBox.balanceOf(token, address(this)).sub(total), - "PrivatePool: Skim too much" + "PrivatePool: skim too much" ); } else { bentoBox.transfer(token, msg.sender, address(this), share); @@ -342,7 +342,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 share ) public { uint256 supplied = userCollateralShare[to]; - require(supplied > 0 || approvedBorrowers[to], "Unapproved borrower"); + require( + supplied > 0 || approvedBorrowers[to], + "PrivatePool: unapproved borrower" + ); userCollateralShare[to] = supplied + share; CollateralBalance memory _collateralBalance = collateralBalance; @@ -456,7 +459,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @dev Concrete implementation of `removeAsset`. function _removeAsset(address to, uint256 share) internal { - require(msg.sender == lender, "Not the lender"); + require(msg.sender == lender, "PrivatePool: not the lender"); // Fits in a uint128 if the transfer goes through: assetBalance.reservesShare = assetBalance.reservesShare.sub( uint128(share) @@ -478,7 +481,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { internal returns (uint256 part, uint256 share) { - require(approvedBorrowers[msg.sender], "Unapproved borrower"); + require( + approvedBorrowers[msg.sender], + "PrivatePool: unapproved borrower" + ); IERC20 _asset = asset; Rebase memory bentoBoxTotals = bentoBox.totals(_asset); AccrueInfo memory _accrueInfo = accrueInfo; @@ -486,9 +492,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { share = bentoBoxTotals.toBase(amount, false); uint256 openFeeAmount = amount.mul(_accrueInfo.BORROW_OPENING_FEE_BPS) / - BASIS_POINTS; - uint256 protocolFeeAmount = openFeeAmount.mul(PROTOCOL_FEE_BPS) / - BASIS_POINTS; + BPS; + uint256 protocolFeeAmount = openFeeAmount.mul(PROTOCOL_FEE_BPS) / BPS; uint256 protocolFeeShare = bentoBoxTotals.toBase( protocolFeeAmount, false @@ -867,7 +872,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 collateralShare = bentoBoxTotals.toBase( debtAmount.mul(_accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul( _exchangeRate - ) / (BASIS_POINTS * EXCHANGE_RATE_PRECISION), + ) / (BPS * EXCHANGE_RATE_PRECISION), false ); @@ -915,9 +920,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { // Math: All collateral fits in 128 bits (BentoBox), so the // multiplications are safe: uint256 excessShare = (allCollateralShare * - (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BASIS_POINTS)) / - BASIS_POINTS; - uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BASIS_POINTS; + (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / BPS; + uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS; uint256 lenderShare = allCollateralShare - excessShare; // (Stack depth): liquidatorShare = excessShare - feeShare; @@ -942,8 +946,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // Charge the protocol fee over the excess. uint256 feeAmount = (allDebtAmount.mul( _accrueInfo.LIQUIDATION_MULTIPLIER_BPS - ) / BASIS_POINTS).sub(allDebtAmount).mul(PROTOCOL_FEE_BPS) / - BASIS_POINTS; // Distribution Amount + ) / BPS).sub(allDebtAmount).mul(PROTOCOL_FEE_BPS) / BPS; // Swap using a swapper freely chosen by the caller // Open (flash) liquidation: get proceeds first and provide the From 11c9a150863ce7fea7aafbd6ad53ed4809a24954 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 10 Dec 2021 04:41:28 +1100 Subject: [PATCH 008/107] Remove some overflow checks with proof --- contracts/PrivatePool.sol | 92 ++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 7ba7f8b0..dd6bd674 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -220,13 +220,22 @@ contract PrivatePool is BoringOwnable, IMasterContract { if (_totalDebt.base == 0) { return; } - uint256 extraAmount = uint256(_totalDebt.elastic) - .mul(_accrueInfo.INTEREST_PER_SECOND) - .mul(elapsedTime) / 1e18; + // No overflow: + // - _totalDebt.elastic is 128 bits + // - INTEREST_PER_SECOND is 64 bits + // - elapsedTime fits in 64 bits for the next 580 billion (10^9) years + uint256 extraAmount = (uint256(_totalDebt.elastic) * + _accrueInfo.INTEREST_PER_SECOND * + elapsedTime) / 1e18; + + // If the interest rate is too high, then this will overflow and + // effectively lock up all funds. Do not set the interest rate too + // high. _totalDebt.elastic = _totalDebt.elastic.add(extraAmount.to128()); totalDebt = _totalDebt; - uint256 feeAmount = extraAmount.mul(PROTOCOL_FEE_BPS) / BPS; + // No overflow: extraAmount is divided by 1e18 > 2^59; we need 16 bits + uint256 feeAmount = (extraAmount * PROTOCOL_FEE_BPS) / BPS; AssetBalance memory _assetBalance = assetBalance; if (_assetBalance.reservesShare == 0) { @@ -267,14 +276,44 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 collateralShare = userCollateralShare[borrower]; if (collateralShare == 0) return false; + // Math overflow/edge case analysis + // + // LHS: + // + // collateralShare: 128 bits (at most the total Bento SHARE balance) + // EXCHANGE_RATE_PRECISION / BPS: lg(1e14) < 47 bits + // COLLATERALIZATION_RATE_BPS: lg(1e4) < 14 bits (max 100% = 10k BPS) + // + // Actually, the product of the latter two fits in 60 bits. + // So what we pass to `toAmount` is safe. In there it gets multiplied + // by the 128-bit total collateral balance. So we need + // + // collateralShare * (total Bento TOKEN balance) + // + // to fit in 196 bits. (See BentoBox contract). This actually has a + // chance of overflowing; if someone were to use the entire supply of + // SPELL as collateral, then 255 bits are used in `toAmount`.j + // + // Be careful of tokens with ridiculously high balances as collateral. + // SPELL is still OK, but even then the BentoBox can't lose too much in + // a strategy: we multiply SHARES by TOKENS at some point, and in + // theory the share/token ratio can swing arbitrarily high toward + // shares. + // + // This happens if the BentoBox loses tokens in a strategy, and then + // receives new tokens in a deposit. Those would then represent a + // large multiple of the current reserves, and thus be alotted a large + // share balance. + // + Rebase memory _totalDebt = totalDebt; return bentoBox.toAmount( collateral, - collateralShare.mul(EXCHANGE_RATE_PRECISION / BPS).mul( - accrueInfo.COLLATERALIZATION_RATE_BPS - ), + collateralShare * + (EXCHANGE_RATE_PRECISION / BPS) * + accrueInfo.COLLATERALIZATION_RATE_BPS, false ) >= // Moved exchangeRate here instead of dividing the other side to @@ -347,11 +386,15 @@ contract PrivatePool is BoringOwnable, IMasterContract { "PrivatePool: unapproved borrower" ); + // No overflow: the total for ALL users fits in 128 bits, or the + // BentoBox transfer (_addTokens) fails. userCollateralShare[to] = supplied + share; CollateralBalance memory _collateralBalance = collateralBalance; - // No over/underflow: it fits in the BentoBox total + // No overflow: the sum fits in the BentoBox total uint256 prevTotal = _collateralBalance.userTotalShare + _collateralBalance.feesEarnedShare; + // No overflow if cast safe: fits in the BentoBox or _addTokens reverts + // Cast safe: _addTokens does not truncate the value collateralBalance.userTotalShare = _collateralBalance.userTotalShare + uint128(share); @@ -364,6 +407,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub( share ); + // No underflow: userTotalShare > userCollateralShare[msg.sender] + // Cast safe: Bento transfer reverts if it is not. collateralBalance.userTotalShare -= uint128(share); emit LogRemoveCollateral(msg.sender, to, share); bentoBox.transfer(collateral, address(this), to, share); @@ -460,7 +505,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @dev Concrete implementation of `removeAsset`. function _removeAsset(address to, uint256 share) internal { require(msg.sender == lender, "PrivatePool: not the lender"); - // Fits in a uint128 if the transfer goes through: + // Cast safe: Bento transfer reverts unless stronger condition holds assetBalance.reservesShare = assetBalance.reservesShare.sub( uint128(share) ); @@ -491,9 +536,14 @@ contract PrivatePool is BoringOwnable, IMasterContract { share = bentoBoxTotals.toBase(amount, false); - uint256 openFeeAmount = amount.mul(_accrueInfo.BORROW_OPENING_FEE_BPS) / + // No overflow: `share` is not modified any more, and fits in the + // BentoBox shares total if the transfer succeeds. But then "amount" + // must fit in the token total; at least up to some rounding error, + // which cannot be more than 128 bits either. + uint256 openFeeAmount = amount * _accrueInfo.BORROW_OPENING_FEE_BPS / BPS; - uint256 protocolFeeAmount = openFeeAmount.mul(PROTOCOL_FEE_BPS) / BPS; + // No overflow: Same reason. Also, we just divided by BPS.. + uint256 protocolFeeAmount = openFeeAmount * PROTOCOL_FEE_BPS / BPS; uint256 protocolFeeShare = bentoBoxTotals.toBase( protocolFeeAmount, false @@ -501,16 +551,26 @@ contract PrivatePool is BoringOwnable, IMasterContract { // The protocol component of the opening fee cannot be owed: AssetBalance memory _assetBalance = assetBalance; + // No overflow on the add: protocolFeeShare < share < Bento total, or + // the transfer reverts. The transfer is independent of the results of + // these calculations: `share` is not modified. + // Theoretically the fee could just make it overflow 128 bits. + // Underflow check is core business logic: _assetBalance.reservesShare = _assetBalance.reservesShare.sub( - (share.add(protocolFeeShare)).to128() + (share + protocolFeeShare).to128() ); - // No overflow if the above succeeded: - // feesEarned + protocolFee <= feesEarned + reserves <= Bento balance + // Cast is safe: `share` fits. Also, the checked cast above succeeded. + // No overflow: protocolFeeShare < reservesShare, and both balances + // together fit in the Bento share balance, _assetBalance.feesEarnedShare += uint128(protocolFeeShare); assetBalance = _assetBalance; - (totalDebt, part) = totalDebt.add(amount.add(openFeeAmount), true); - borrowerDebtPart[msg.sender] = borrowerDebtPart[msg.sender].add(part); + // No overflow (inner add): amount fits in 128 bits, as shown before, + // and openFeeAmount is less. + (totalDebt, part) = totalDebt.add(amount + openFeeAmount, true); + // No overflow: totalDebt.base is the sum of all borrowerDebtParts, + // fits in 128 bits and just had "part" added to it. + borrowerDebtPart[msg.sender] += part; emit LogBorrow(msg.sender, to, amount, openFeeAmount, part); bentoBox.transfer(_asset, address(this), to, share); From a2d8530cea16db3ec6fcbaf6330806b58ff52618 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 10 Dec 2021 04:49:09 +1100 Subject: [PATCH 009/107] Add deployment script and some tests --- contracts/mocks/BentoBoxMock.sol | 5 + deploy/PrivatePool.ts | 44 ++ package.json | 1 + test/PrivatePool.forking.test.ts | 163 ++++++ test/PrivatePool.test.ts | 820 +++++++++++++++++++++++++++++++ test/PrivatePool.ts | 32 ++ test/constants.mainnet.ts | 2 + utilities/time.ts | 8 + 8 files changed, 1075 insertions(+) create mode 100644 deploy/PrivatePool.ts create mode 100644 test/PrivatePool.forking.test.ts create mode 100644 test/PrivatePool.test.ts create mode 100644 test/PrivatePool.ts create mode 100644 test/constants.mainnet.ts diff --git a/contracts/mocks/BentoBoxMock.sol b/contracts/mocks/BentoBoxMock.sol index c105f562..cb4645a8 100644 --- a/contracts/mocks/BentoBoxMock.sol +++ b/contracts/mocks/BentoBoxMock.sol @@ -12,4 +12,9 @@ contract BentoBoxMock is BentoBoxV1 { token.safeTransferFrom(msg.sender, address(this), amount); totals[token].addElastic(amount); } + + function takeLoss(IERC20 token, uint256 amount) public { + token.safeTransfer(msg.sender, amount); + totals[token].subElastic(amount); + } } diff --git a/deploy/PrivatePool.ts b/deploy/PrivatePool.ts new file mode 100644 index 00000000..4c6f7225 --- /dev/null +++ b/deploy/PrivatePool.ts @@ -0,0 +1,44 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { ethers, network } from "hardhat"; +import { BentoBoxMock } from "../typechain"; +import { DeploymentSubmission } from "hardhat-deploy/dist/types"; +import { expect } from "chai"; + +const deployFunction: DeployFunction = async function ( + hre: HardhatRuntimeEnvironment +) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + + const { deployer } = await getNamedAccounts(); + + // Original BentoBox on mainnet: + const bentoBoxAddress = "0xf5bce5077908a1b7370b9ae04adc565ebd643966"; + + await deploy("PrivatePool", { + from: deployer, + args: [bentoBoxAddress], + log: true, + deterministicDeployment: false, + }); +}; + +export default deployFunction; + +if (network.name !== "hardhat" || process.env.HARDHAT_LOCAL_NODE) { + console.log("SKI-AP!"); + deployFunction.skip = ({ getChainId }) => + new Promise((resolve, reject) => { + try { + getChainId().then((chainId) => { + resolve(chainId !== "1"); + }); + } catch (error) { + reject(error); + } + }); +} + +deployFunction.tags = ["PrivatePool"]; +deployFunction.dependencies = []; diff --git a/package.json b/package.json index 3c1ac7be..c3e32206 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "hardhat-gas-reporter": "^1.0.4", "husky": "^7.0.2", "lint-staged": "^11.2.3", + "lodash": "^4.17.21", "prettier": "^2.4.1", "prettier-plugin-solidity": "^1.0.0-beta.18", "pretty-quick": "^3.1.1", diff --git a/test/PrivatePool.forking.test.ts b/test/PrivatePool.forking.test.ts new file mode 100644 index 00000000..965fd6b8 --- /dev/null +++ b/test/PrivatePool.forking.test.ts @@ -0,0 +1,163 @@ +import { + ethers, + network, + deployments, + getNamedAccounts, + artifacts, +} from "hardhat"; +import { expect } from "chai"; +import { BigNumberish, Signer } from "ethers"; +import _ from "lodash"; + +import { advanceTime, getBigNumber, impersonate } from "../utilities"; +import { BentoBoxV1, PrivatePool } from "../typechain"; + +import { WETH9, USDC } from "./constants.mainnet"; + +const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; + +const WETH_WHALE = '0x6555e1CC97d3cbA6eAddebBCD7Ca51d75771e0B8'; +const USDC_WHALE = '0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503'; +const GENERAL_WHALE = '0x84D34f4f83a87596Cd3FB6887cFf8F17Bf5A7B83'; + +const initTypes = { + collateral: "address", + asset: "address", + oracle: "address", + oracleData: "bytes", + lender: "address", + borrowers: "address[]", + INTEREST_PER_SECOND: "uint64", + NO_LIQUIDATIONS_BEFORE: "uint64", + COLLATERALIZATION_RATE_BPS: "uint16", + LIQUIDATION_MULTIPLIER_BPS: "uint16", + BORROW_OPENING_FEE_BPS: "uint16", + LIQUIDATION_SEIZE_COLLATERAL: "bool", +}; +const typeDefaults = { + address: ZERO_ADDR, + "address[]": [], + bytes: "", +}; +// These rely on JS/TS iterating over the keys in the order they were defined: +const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); +const encodeInitData = (kvs) => + ethers.utils.defaultAbiCoder.encode( + [`tuple(${initTypeString})`], + [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] + ); + +const getSignerFor = async (addr) => { + await impersonate(addr); + return ethers.getSigner(addr); +}; + +describe("Private Lending Pool - Forked Mainnet", async () => { + if (process.env.FORKING !== 'true') { return; } + let snapshotId; + let masterContract: PrivatePool; + let bentoBox: BentoBoxV1; + let pairContract: PrivatePool; + let wethWhale: Signer; + let usdcWhale: Signer; + let generalWhale: Signer; + + + const deployPair = async (initSettings) => { + const deployTx = await bentoBox + .deploy(masterContract.address, encodeInitData(initSettings), false) + .then((tx) => tx.wait()); + const [deployEvent] = deployTx.events; + expect(deployEvent.eventSignature).to.equal( + "LogDeploy(address,bytes,address)" + ); + const { cloneAddress } = deployEvent.args; + return ethers.getContractAt("PrivatePool", cloneAddress); + }; + + before(async () => { + const alchemyKey = process.env.ALCHEMY_API_KEY; + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: + process.env.ETHEREUM_RPC_URL || + `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, + blockNumber: 13715035, + }, + }, + ], + }); + + await deployments.fixture(["PrivatePool"]); + masterContract = await ethers.getContract("PrivatePool"); + bentoBox = await ethers.getContractAt( + "BentoBoxV1", + await masterContract.bentoBox() + ); + + const sevenPercentAnnually = getBigNumber(7).div(100 * 3600 * 24 * 365); + pairContract = await deployPair({ + lender: USDC_WHALE, + borrowers: [WETH_WHALE, GENERAL_WHALE], + asset: USDC, + collateral: WETH9, + INTEREST_PER_SECOND: sevenPercentAnnually, + COLLATERALIZATION_RATE_BPS: 7500, + LIQUIDATION_MULTIPLIER_BPS: 11200, + BORROW_OPENING_FEE_BPS: 10, + }); + + snapshotId = await ethers.provider.send("evm_snapshot", []); + + wethWhale = await getSignerFor(WETH_WHALE); + usdcWhale = await getSignerFor(WETH_WHALE); + generalWhale = await getSignerFor(GENERAL_WHALE); + }); + + afterEach(async () => { + await network.provider.send("evm_revert", [snapshotId]); + snapshotId = await ethers.provider.send("evm_snapshot", []); + }); + + describe("Deploy", async () => { + it("Should deploy", async () => { + expect(await pairContract.lender()).to.equal(USDC_WHALE); + for (const addr of [GENERAL_WHALE, WETH_WHALE]) { + expect(await pairContract.approvedBorrowers(addr)).to.equal(true); + } + expect(await pairContract.approvedBorrowers(USDC_WHALE)).to.equal(false); + }); + + it("Should reject bad settings", async () => { + await expect( + deployPair({ + collateral: ZERO_ADDR, + }) + ).to.be.revertedWith("PrivatePool: bad pair"); + + await expect( + deployPair({ + collateral: WETH9, + LIQUIDATION_MULTIPLIER_BPS: 9_999, + }) + ).to.be.revertedWith("PrivatePool: negative liquidation bonus"); + + await expect( + deployPair({ + collateral: WETH9, + LIQUIDATION_MULTIPLIER_BPS: 10_000, + COLLATERALIZATION_RATE_BPS: 10_001, + }) + ).to.be.revertedWith("PrivatePool: bad collateralization rate"); + }); + + it("Should refuse to initialize twice", async () => { + await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( + "PrivatePool: already initialized" + ); + }); + }); +}); diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts new file mode 100644 index 00000000..d7a1a07f --- /dev/null +++ b/test/PrivatePool.test.ts @@ -0,0 +1,820 @@ +import { + ethers, + network, + deployments, + getNamedAccounts, + artifacts, +} from "hardhat"; +import { expect } from "chai"; +import { BigNumberish, Signer } from "ethers"; + +import { + advanceNextTime, + duration, + encodeParameters, + getBigNumber, + impersonate, +} from "../utilities"; +import { + BentoBoxMock, + ERC20Mock, + OracleMock, + WETH9Mock, + PrivatePool, +} from "../typechain"; +import { encodeInitData } from "./PrivatePool"; + +const { formatUnits } = ethers.utils; +const { MaxUint256, AddressZero, HashZero } = ethers.constants; + +// Cook actions +const Cook = { + ACTION_ADD_ASSET: 1, + ACTION_REPAY: 2, + ACTION_REMOVE_ASSET: 3, + ACTION_REMOVE_COLLATERAL: 4, + ACTION_BORROW: 5, + ACTION_GET_REPAY_SHARE: 6, + ACTION_GET_REPAY_PART: 7, + ACTION_ACCRUE: 8, + + // Functions that don't need accrue to be called + ACTION_ADD_COLLATERAL: 10, + ACTION_UPDATE_EXCHANGE_RATE: 11, + + // Function on BentoBox + ACTION_BENTO_DEPOSIT: 20, + ACTION_BENTO_WITHDRAW: 21, + ACTION_BENTO_TRANSFER: 22, + ACTION_BENTO_TRANSFER_MULTIPLE: 23, + ACTION_BENTO_SETAPPROVAL: 24, + + // Any external call (except to BentoBox) + ACTION_CALL: 30, + + USE_VALUE1: -1, + USE_VALUE2: -2, +}; + +const MainTestSettings = { + // 7% annually -- precision is 18 digits: + INTEREST_PER_SECOND: getBigNumber(7).div(100 * 3600 * 24 * 365), + COLLATERALIZATION_RATE_BPS: 7500, + LIQUIDATION_MULTIPLIER_BPS: 11200, + BORROW_OPENING_FEE_BPS: 10, +}; + +describe("Private Lending Pool", async () => { + let weth: WETH9Mock; + let guineas: ERC20Mock; + let bentoBox: BentoBoxMock; + let oracle: OracleMock; + let masterContract: PrivatePool; + let pairContract: PrivatePool; + let alice: Signer; + let bob: Signer; + let carol: Signer; + + // Inner snapshots.The "inner" state is whatever `before` in `proc` sets up. + // Reverts to the "outer" state after. + const describeSnapshot = (name, proc) => + describe(name, () => { + let outerSnapshotId; + before(async () => { + outerSnapshotId = await ethers.provider.send("evm_snapshot", []); + }); + + proc(); + + let snapshotId = null; + beforeEach(async () => { + if (snapshotId) { + await ethers.provider.send("evm_revert", [snapshotId]); + } + snapshotId = await ethers.provider.send("evm_snapshot", []); + }); + + after(async () => { + await ethers.provider.send("evm_revert", [outerSnapshotId]); + }); + }); + + const deployPair = async (initSettings) => { + const deployTx = await bentoBox + .deploy(masterContract.address, encodeInitData(initSettings), false) + .then((tx) => tx.wait()); + const [deployEvent] = deployTx.events; + expect(deployEvent.eventSignature).to.equal( + "LogDeploy(address,bytes,address)" + ); + const { cloneAddress } = deployEvent.args; + return ethers.getContractAt("PrivatePool", cloneAddress); + }; + + const showBalances = async (accs = { alice, bob, carol }) => { + for (const [name, acc] of Object.entries(accs)) { + console.log(`\n---- ${name}:`); + const ethBalance = await ethers.provider.getBalance(acc.address); + console.log(`Ethereum: ${formatUnits(ethBalance)}\n`); + for (const [token, contract] of [ + ["WETH", weth], + ["Guineas", guineas], + ]) { + const balance = await contract.balanceOf(acc.address); + const bentoShares = await bentoBox.balanceOf( + contract.address, + acc.address + ); + const bentoBalance = await bentoBox.toAmount( + contract.address, + bentoShares, + false + ); + console.log(`${token}: ${formatUnits(balance)}`); + console.log( + `${token} (BentoBox):`, + `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n` + ); + } + } + }; + + before(async () => { + const WETH9Mock = await ethers.getContractFactory("WETH9Mock"); + weth = await WETH9Mock.deploy(); + + const BentoBoxMock = await ethers.getContractFactory("BentoBoxMock"); + bentoBox = await BentoBoxMock.deploy(weth.address); + + const PrivatePool = await ethers.getContractFactory("PrivatePool"); + masterContract = await PrivatePool.deploy(bentoBox.address); + await bentoBox.whitelistMasterContract(masterContract.address, true); + + const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); + guineas = await ERC20Mock.deploy(getBigNumber(1_000_000)); + + const OracleMock = await ethers.getContractFactory("OracleMock"); + oracle = await OracleMock.deploy(); + + const addresses = await getNamedAccounts(); + alice = await ethers.getSigner(addresses.alice); + bob = await ethers.getSigner(addresses.bob); + carol = await ethers.getSigner(addresses.carol); + + const mc = masterContract.address; + const hz = HashZero; + for (const signer of [alice, bob, carol]) { + const addr = signer.address; + const bb = bentoBox.connect(signer); + await bb.setMasterContractApproval(addr, mc, true, 0, hz, hz); + + await weth.connect(signer).deposit({ value: getBigNumber(1000) }); + await guineas.transfer(addr, getBigNumber(10_000)); + + await weth.connect(signer).approve(bentoBox.address, MaxUint256); + await bb.deposit(weth.address, addr, addr, getBigNumber(200), 0); + + await guineas.connect(signer).approve(bentoBox.address, MaxUint256); + await bb.deposit(guineas.address, addr, addr, getBigNumber(3000), 0); + } + await guineas.approve(bentoBox.address, MaxUint256); + await bentoBox.addProfit(guineas.address, getBigNumber(11000)); + + await weth.approve(bentoBox.address, MaxUint256); + await weth.deposit({ value: getBigNumber(100) }); + const dep = addresses.deployer; + await bentoBox.deposit(weth.address, dep, dep, getBigNumber(100), 0); + await bentoBox.takeLoss(weth.address, getBigNumber(169)); + + // WETH: 700 deposited (200 each), 169 lost => 700 shares is 531 WETH + // Guineas: 9000 in, 11k profit => 9k shares is 20k guineas + + // Initial balances are now: + // + // ---- alice: + // Ethereum: 9000.0 + // + // WETH: 800.0 + // WETH (BentoBox): 151.714285714285714285 (200.0 shares) + // + // Guineas: 7000.0 + // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) + // + // ---- bob: + // Ethereum: 9000.0 + // + // WETH: 800.0 + // WETH (BentoBox): 151.714285714285714285 (200.0 shares) + // + // Guineas: 7000.0 + // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) + // + // ---- carol: + // Ethereum: 9000.0 + // + // WETH: 800.0 + // WETH (BentoBox): 151.714285714285714285 (200.0 shares) + // + // Guineas: 7000.0 + // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) + + pairContract = await deployPair({ + lender: alice.address, + borrowers: [bob.address, carol.address], + asset: guineas.address, + collateral: weth.address, + oracle: oracle.address, + ...MainTestSettings, + }); + }); + + describe("Deploy", async () => { + it("Should deploy", async () => { + expect(await pairContract.lender()).to.equal(alice.address); + for (const { address } of [carol, bob]) { + expect(await pairContract.approvedBorrowers(address)).to.equal(true); + } + expect(await pairContract.approvedBorrowers(alice.address)).to.equal( + false + ); + }); + + it("Should reject bad settings", async () => { + await expect( + deployPair({ + collateral: AddressZero, + }) + ).to.be.revertedWith("PrivatePool: bad pair"); + + await expect( + deployPair({ + collateral: weth.address, + LIQUIDATION_MULTIPLIER_BPS: 9_999, + }) + ).to.be.revertedWith("PrivatePool: negative liquidation bonus"); + + await expect( + deployPair({ + collateral: weth.address, + LIQUIDATION_MULTIPLIER_BPS: 10_000, + COLLATERALIZATION_RATE_BPS: 10_001, + }) + ).to.be.revertedWith("PrivatePool: bad collateralization rate"); + }); + + it("Should refuse to initialize twice", async () => { + await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( + "PrivatePool: already initialized" + ); + }); + }); + + describeSnapshot("Add Asset", async () => { + it("Should let the lender add assets", async () => { + const share = getBigNumber(450); + await expect(pairContract.connect(alice).addAsset(false, share)) + .to.emit(pairContract, "LogAddAsset") + .withArgs(alice.address, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pairContract.address, share); + + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should let the lender add assets (skim)", async () => { + // This is not a reasonable transaction.. + const share = getBigNumber(450); + const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + + await bentoBox.connect(alice).transfer(g, a, p, share); + await expect(pairContract.connect(alice).addAsset(true, share)) + .to.emit(pairContract, "LogAddAsset") + .withArgs(bentoBox.address, share); + + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should let the lender add assets (cook amount)", async () => { + // ( 10^9 ) ( 10^9 ) + const amount = 27_182_818_284_590_452_353n; // Does not divide 20 or 9 + + // (Shares : Amount) in Bento is (9 : 20) + // This is what the BentoBox gives us for our deposit; round down: + const share = (amount * 9n) / 20n; + + const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; + const datas = [ + encodeParameters( + ["address", "address", "uint256", "uint256"], + [g, a, amount, 0] + ), + encodeParameters(["int256", "bool"], [share, false]), + ]; + const values = [0, 0]; + + // Make sure the existing Bento balance stays the same: + const initialBentoBalance = await bentoBox.balanceOf(g, a); + + await expect(pairContract.connect(alice).cook(actions, values, datas)) + .to.emit(bentoBox, "LogDeposit") + .withArgs(g, a, a, amount, share) + .to.emit(pairContract, "LogAddAsset") + .withArgs(a, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(g, a, p, share); + + expect(await bentoBox.balanceOf(g, a)).to.equal(initialBentoBalance); + + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(share); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should refuse to skim too much", async () => { + const share = getBigNumber(123); + const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + + await bentoBox.connect(alice).transfer(g, a, p, share); + await expect( + pairContract.connect(alice).addAsset(true, share.add(1)) + ).to.be.revertedWith("PrivatePool: skim too much"); + }); + + it("Should let anyone add assets", async () => { + const share = getBigNumber(450); + await expect(pairContract.connect(bob).addAsset(false, share)) + .to.emit(pairContract, "LogAddAsset") + .withArgs(bob.address, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, bob.address, pairContract.address, share); + + const share2 = 27_182_818_284_590_452_353n; + await expect(pairContract.connect(carol).addAsset(false, share2)) + .to.emit(pairContract, "LogAddAsset") + .withArgs(carol.address, share2) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, carol.address, pairContract.address, share2); + + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(share.add(share2)); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + }); + + describeSnapshot("Add Collateral", async () => { + it("Should let approved borrowers add collateral", async () => { + const share1 = getBigNumber(55); + const to1 = bob.address; + await expect(pairContract.connect(bob).addCollateral(to1, false, share1)) + .to.emit(pairContract, "LogAddCollateral") + .withArgs(bob.address, to1, share1) + .to.emit(bentoBox, "LogTransfer") + .withArgs(weth.address, bob.address, pairContract.address, share1); + + expect(await pairContract.userCollateralShare(to1)).to.equal(share1); + + let collateralBalance = await pairContract.collateralBalance(); + expect(collateralBalance.userTotalShare).to.equal(share1); + expect(collateralBalance.feesEarnedShare).to.equal(0); + + const share2 = 27_182_818_284_590_452_353n; + const to2 = carol.address; + await expect( + pairContract.connect(carol).addCollateral(to2, false, share2) + ) + .to.emit(pairContract, "LogAddCollateral") + .withArgs(carol.address, to2, share2) + .to.emit(bentoBox, "LogTransfer") + .withArgs(weth.address, carol.address, pairContract.address, share2); + + expect(await pairContract.userCollateralShare(to2)).to.equal(share2); + + collateralBalance = await pairContract.collateralBalance(); + expect(collateralBalance.userTotalShare).to.equal(share1.add(share2)); + expect(collateralBalance.feesEarnedShare).to.equal(0); + }); + + it("Should let anyone add collateral for approved borrowers", async () => { + const share = getBigNumber(55); + const to = bob.address; + await expect(pairContract.connect(alice).addCollateral(to, false, share)) + .to.emit(pairContract, "LogAddCollateral") + .withArgs(alice.address, to, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(weth.address, alice.address, pairContract.address, share); + }); + + it("Should refuse collateral for unapproved borrowers", async () => { + const share = getBigNumber(55); + const to = alice.address; + await expect( + pairContract.connect(bob).addCollateral(to, false, share) + ).to.be.revertedWith("PrivatePool: unapproved borrower"); + }); + + it("Should let approved borrowers add collateral (skim)", async () => { + const share = getBigNumber(55); + const to = bob.address; + const [w, b, p] = [weth, bob, pairContract].map((x) => x.address); + + await bentoBox.connect(bob).transfer(w, b, p, share); + await expect(pairContract.connect(bob).addCollateral(to, true, share)) + .to.emit(pairContract, "LogAddCollateral") + .withArgs(bentoBox.address, to, share); + }); + }); + + describeSnapshot("Borrow", async () => { + const assetShare = getBigNumber(1000).mul(9).div(20); + const collatShare1 = getBigNumber(31_415926535_897932384n, 0); + const collatShare2 = getBigNumber(27_182818284_590452353n, 0); + + const rate = getBigNumber(1, 18).div(12); // one WETH is 12 guineas + const ratePrecision = getBigNumber(1); + + before(async () => { + await pairContract.connect(alice).addAsset(false, assetShare); + + const to1 = bob.address; + await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + + const to2 = carol.address; + await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + + await oracle.set(rate); + await pairContract.updateExchangeRate(); + }); + + it("Should allow approved borrowers to borrow", async () => { + const amount = getBigNumber(10); + expect(amount.mul(rate)).to.be.lte(amount.mul(ratePrecision)); + + const fee = amount.div(1000); + const part = amount.add(fee); // total debt started at zero + + // Still 9 : 20 ratio + const share = amount.mul(9).div(20); + + const [g, b, p] = [guineas, bob, pairContract].map((x) => x.address); + await expect(pairContract.connect(bob).borrow(b, amount)) + .to.emit(pairContract, "LogBorrow") + .withArgs(b, b, amount, fee, part) + .to.emit(bentoBox, "LogTransfer") + .withArgs(g, p, b, share); + + const totalDebt = await pairContract.totalDebt(); + expect(totalDebt.elastic).to.equal(part); + expect(totalDebt.base).to.equal(part); + + expect(await pairContract.borrowerDebtPart(bob.address)).to.equal(part); + }); + + it("Should refuse to lend to unapproved borrowers", async () => { + await expect( + pairContract.connect(alice).borrow(alice.address, 1) + ).to.be.revertedWith("PrivatePool: unapproved borrower"); + }); + + it("Should enforce LTV requirements when borrowing", async () => { + // Pinning down an exact cutoff is not as straightforward as it seems; + // the calculation involves the BentoBox balance and token/share ratio of + // the collateral (from anyone), total debt taken out by others, and + // total accrued interest. + + const collatAmount = collatShare1.mul(531).div(700); // WETH ratio + const borrowAmount = collatAmount.mul(9); // 75% of 12 + + await expect( + pairContract.connect(bob).borrow(bob.address, borrowAmount) + ).to.be.revertedWith("PrivatePool: borrower insolvent"); + + // Accounting for the 0.1% open fee is enough to make it succeed: + const withFee = borrowAmount.mul(1000).div(1001); + await expect( + pairContract.connect(bob).borrow(bob.address, withFee) + ).to.emit(pairContract, "LogBorrow"); + + // Borrowing even one more wei is enough to make it fail again: + await expect( + pairContract.connect(bob).borrow(bob.address, withFee.add(1)) + ).to.be.revertedWith("PrivatePool: borrower insolvent"); + }); + + it("Should collect the protocol fee immediately", async () => { + // Borrowers incur an opening fee, which gets added on to their debt. The + // protocol gets a cut of that. The lender pays for this as soon as + // feasible. If there are assets left, it comes out of those. + // + // This is the case in the initial setup of this section. + const borrowAmount = getBigNumber(100); + const openFee = borrowAmount.div(1000); + const protocolFee = openFee.div(10); + + // No interest has accrued yet, so this is corresponds 1:1 with tokens: + const debtPart = borrowAmount.add(openFee); + + const borrowShare = borrowAmount.mul(9).div(20); + const protocolFeeShare = protocolFee.mul(9).div(20); + const takenShare = borrowShare.add(protocolFeeShare); + + await expect( + pairContract.connect(bob).borrow(bob.address, borrowAmount) + ).to.emit(pairContract, "LogBorrow"); + + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(assetShare.sub(takenShare)); + expect(assetBalance.feesEarnedShare).to.equal(protocolFeeShare); + + expect(await pairContract.feesOwedAmount()).to.equal(0); + }); + + it("Should not lend out more than there is", async () => { + // There are 1000 guineas of assets; 150 WETH allows for borrowing + // 1350 and is therefore enough: + await pairContract + .connect(bob) + .addCollateral(bob.address, false, getBigNumber(150)); + + // More than reserves + await expect( + pairContract.connect(bob).borrow(bob.address, getBigNumber(1001)) + ).to.be.revertedWith("BoringMath: Underflow"); + }); + + it("Should not defer the protocol fee on new loans", async () => { + // This amounts to testing that the amount + protocol fee need to be in + // reserve: + await pairContract + .connect(bob) + .addCollateral(bob.address, false, getBigNumber(150)); + + // Exactly the reserves (our test amounts divide cleanly), but that is + // not enough with the fee: + const reservesAmount = getBigNumber(1000); + await expect( + pairContract.connect(bob).borrow(bob.address, reservesAmount) + ).to.be.revertedWith("BoringMath: Underflow"); + + const cutoff = reservesAmount.mul(1000).div(1001); + await expect( + pairContract.connect(bob).borrow(bob.address, cutoff) + ).to.emit(pairContract, "LogBorrow"); + }); + }); + + describeSnapshot("Accrue", async () => { + // Potentially overcollateralize, so we can test what happens if fees cannot + // be taken out of reserves. + const assetAmount = getBigNumber(1000); + const assetShare = assetAmount.mul(9).div(20); + + // Each enough to borrow all the assets: + const collatShare1 = getBigNumber(200); + const collatShare2 = getBigNumber(200); + + const borrowAmount1 = getBigNumber(100); + const openFee1 = borrowAmount1.div(1000); + const debtAmount1 = borrowAmount1.add(openFee1); + + const rate = getBigNumber(1, 18).div(10); // one WETH is 10 guineas now + const ratePrecision = getBigNumber(1, 18); + + const YEAR = 3600 * 24 * 365; + + before(async () => { + await pairContract.connect(alice).addAsset(false, assetShare); + + const to1 = bob.address; + await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + + const to2 = carol.address; + await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + + await oracle.set(rate); + await pairContract.updateExchangeRate(); + }); + + it("Should charge interest and collect fees over it", async () => { + await pairContract.connect(bob).borrow(bob.address, borrowAmount1); + await advanceNextTime(YEAR); + + const perSecond = MainTestSettings.INTEREST_PER_SECOND; + const extraAmount = debtAmount1 + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(YEAR) + .div(getBigNumber(1)); + const feeAmount = extraAmount.div(10); + + await expect(pairContract.accrue()) + .to.emit(pairContract, "LogAccrue") + .withArgs(extraAmount, feeAmount); + + // Protocol cut of the open fee + fee on interest, both in shares: + expect((await pairContract.assetBalance()).feesEarnedShare).to.equal( + openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20)) + ); + expect(await pairContract.feesOwedAmount()).to.equal(0); + + const totalDebt = await pairContract.totalDebt(); + expect(totalDebt.base).to.equal(debtAmount1); + expect(totalDebt.elastic).to.equal(debtAmount1.add(extraAmount)); + + // Seven percent (hardcoded) on the amount + 0.1% opening fee. Despite + // all the rounding, this should be roughly 0.07007 times the amount + // taken out. Since the contract rounds down, we expect to be under, so + // round up for the test: + expect( + extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1) + ).to.equal(7007); + }); + + it("Should not do anything if nothing is borrowed", async () => { + await pairContract.accrue(); + // No "LogAccrue" event. Cleaner way to do this? + expect( + await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }]) + ).to.deep.equal([]); + }); + + it("Should defer fees if everything is loaned out", async () => { + // We effect this by taking a ridiculously long time period, so that even + // the protocol fee drains the remaining asset reserves. Further fees + // should be recorded as "owed". + const almostEverything = assetAmount.mul(99).div(100); + const openFee = almostEverything.div(1000); + const openProtocolFeeShare = openFee.div(10).mul(9).div(20); + const remainingShare = assetShare + .sub(almostEverything.mul(9).div(20)) + .sub(openProtocolFeeShare); + const initialDebt = almostEverything.add(openFee); + + await pairContract.connect(bob).borrow(bob.address, almostEverything); + const time = 1000 * YEAR; + await advanceNextTime(time); + + // 7000% interest; the fee is 7% of the initial debt, which is more than + // remaining asset reserves. It should still work: + const perSecond = MainTestSettings.INTEREST_PER_SECOND; + const extraAmount = initialDebt + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(time) + .div(getBigNumber(1)); + const feeAmount = extraAmount.div(10); + await expect(pairContract.accrue()) + .to.emit(pairContract, "LogAccrue") + .withArgs(extraAmount, feeAmount); + + // Outstanding debt is recorded normally: + const totalDebt = await pairContract.totalDebt(); + expect(totalDebt.base).to.equal(initialDebt); + expect(totalDebt.elastic).to.equal(initialDebt.add(extraAmount)); + + // Reserves are drained: what wasn't loaned out was collected as fees. + // These already included the protocol fee: + const assetBalance = await pairContract.assetBalance(); + expect(assetBalance.reservesShare).to.equal(0); + expect(assetBalance.feesEarnedShare).to.equal( + remainingShare.add(openProtocolFeeShare) + ); + + // We collected the remaining asset reserves as fee. The rest is owed: + const feeShare = feeAmount.mul(9).div(20); + const stillOwedShare = feeShare.sub(remainingShare); + const stillOwedAmount = stillOwedShare.mul(20).div(9); + expect(await pairContract.feesOwedAmount()).to.equal(stillOwedAmount); + }); + }); + + describeSnapshot("Remove Collateral", async () => { + const assetShare = getBigNumber(1000).mul(9).div(20); + const collatShare1 = getBigNumber(31_415926535_897932384n, 0); + const collatShare2 = getBigNumber(27_182818284_590452353n, 0); + + const rate = getBigNumber(1, 18).div(12); // one WETH is 12 guineas + const ratePrecision = getBigNumber(1); + + before(async () => { + await pairContract.connect(alice).addAsset(false, assetShare); + + const to1 = bob.address; + await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + + const to2 = carol.address; + await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + + await oracle.set(rate); + await pairContract.updateExchangeRate(); + + // await pairContract.connect(carol).borrow(carol.address, getBigNumber(100)) + }); + + it("Should let anyone with collateral remove it", async () => { + // The only case where "anyone" is not a borrower is (currently) pairs + // with "seize collateral"-type liquidations; then it's the lender. This + // may change if we allow modifying the whitelist; then we'll have to + // cleanly handle no-longer-whitelisted users. + expect( + await pairContract + .connect(bob) + .removeCollateral(bob.address, collatShare1) + ) + .to.emit(pairContract, "LogRemoveCollateral") + .withArgs(bob.address, bob.address, collatShare1); + + const remainder = getBigNumber(12); + expect( + await pairContract + .connect(carol) + .removeCollateral(carol.address, collatShare2.sub(remainder)) + ) + .to.emit(pairContract, "LogRemoveCollateral") + .withArgs(carol.address, carol.address, collatShare2.sub(remainder)); + }); + + // (All this is pretty much untouched since Kashi.. OK if accrue() and + // isSolvent are OK). + }); + + describeSnapshot("Edge Cases", () => { + const makeIsSolventTest = (totalSupply, shouldPass) => async () => { + const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); + const OracleMock = await ethers.getContractFactory("OracleMock"); + + const sdOracle = await OracleMock.deploy(); + await sdOracle.set(getBigNumber(100)); // 100 coins to the dollar; 1c + const enoughDollars = getBigNumber(1_000, 0).mul(totalSupply); + + const coin = await ERC20Mock.deploy(totalSupply); + const dollar = await ERC20Mock.deploy(enoughDollars); + + await coin.transfer(bob.address, totalSupply); + await dollar.transfer(alice.address, totalSupply.div(10)); // Also Enough + + const coinPair = await deployPair({ + lender: alice.address, + borrowers: [bob.address], + asset: dollar.address, + collateral: coin.address, + oracle: sdOracle.address, + ...MainTestSettings, // (Mostly defaults from Kashi) + }); + await coinPair.updateExchangeRate(); + + await dollar.connect(alice).approve(bentoBox.address, MaxUint256); + await bentoBox + .connect(alice) + .deposit( + dollar.address, + alice.address, + alice.address, + totalSupply.div(10), + 0 + ); + await coinPair.connect(alice).addAsset(false, totalSupply.div(10)); + + await coin.connect(bob).approve(bentoBox.address, MaxUint256); + await bentoBox + .connect(bob) + .deposit(coin.address, bob.address, bob.address, totalSupply, 0); + await coinPair + .connect(bob) + .addCollateral(bob.address, false, totalSupply); + + const assetBalance = await coinPair.assetBalance(); + const collateralBalance = await coinPair.collateralBalance(); + const bobCollateral = await coinPair.userCollateralShare(bob.address); + + expect(assetBalance.reservesShare).to.equal(totalSupply.div(10)); + expect(collateralBalance.userTotalShare).to.equal(totalSupply); + expect(bobCollateral).to.equal(totalSupply); + + const bentoCoinTotals = await bentoBox.totals(coin.address); + expect(bentoCoinTotals.elastic).to.equal(totalSupply); + + if (shouldPass) { + await expect(coinPair.connect(bob).borrow(bob.address, 1)) + .to.emit(coinPair, "LogBorrow") + .to.emit(bentoBox, "LogTransfer"); + } else { + await expect( + coinPair.connect(bob).borrow(bob.address, 1) + ).to.be.revertedWith("BoringMath: Mul Overflow"); + } + }; + + it( + "Works with all SPELL as collateral (no strat losses)", + makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true) + ); + + it( + "Breaks at 393 billion (18-decimal) tokens", + makeIsSolventTest(getBigNumber(393_000_000_000n, 18), false) + ); + }); +}); diff --git a/test/PrivatePool.ts b/test/PrivatePool.ts new file mode 100644 index 00000000..67aeb42f --- /dev/null +++ b/test/PrivatePool.ts @@ -0,0 +1,32 @@ +import { ethers } from "hardhat"; +import _ from "lodash"; + +const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; + +const initTypes = { + collateral: "address", + asset: "address", + oracle: "address", + oracleData: "bytes", + lender: "address", + borrowers: "address[]", + INTEREST_PER_SECOND: "uint64", + NO_LIQUIDATIONS_BEFORE: "uint64", + COLLATERALIZATION_RATE_BPS: "uint16", + LIQUIDATION_MULTIPLIER_BPS: "uint16", + BORROW_OPENING_FEE_BPS: "uint16", + LIQUIDATION_SEIZE_COLLATERAL: "bool", +}; +const typeDefaults = { + address: ZERO_ADDR, + "address[]": [], + bytes: "", +}; +// These rely on JS/TS iterating over the keys in the order they were defined: +const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); + +export const encodeInitData = (kvs) => + ethers.utils.defaultAbiCoder.encode( + [`tuple(${initTypeString})`], + [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] + ); diff --git a/test/constants.mainnet.ts b/test/constants.mainnet.ts new file mode 100644 index 00000000..436239b8 --- /dev/null +++ b/test/constants.mainnet.ts @@ -0,0 +1,2 @@ +export const WETH9 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; +export const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; diff --git a/utilities/time.ts b/utilities/time.ts index 1b76e4f3..8d4c8890 100644 --- a/utilities/time.ts +++ b/utilities/time.ts @@ -32,6 +32,14 @@ export async function advanceTime(time) { await advanceBlock() } +// Does not mine any blocks, and wall clock time has no effect. +export async function advanceNextTime(offset) { + const { timestamp } = await ethers.provider.getBlock("latest") + const next = timestamp + offset + await ethers.provider.send("evm_setNextBlockTimestamp", [next]) + return next +} + export const duration = { seconds: function (val) { return BigNumber.from(val) From 9a2a0873b85e18fddd9f297662b3e49834f48dc5 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 10 Dec 2021 07:34:42 +1100 Subject: [PATCH 010/107] Use full math in isSolvent check --- contracts/PrivatePool.sol | 141 +++++++++++++++++++++++-------- contracts/libraries/FullMath.sol | 2 +- test/PrivatePool.test.ts | 5 +- 3 files changed, 110 insertions(+), 38 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index dd6bd674..a9a2cf58 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -27,6 +27,7 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IOracle.sol"; import "./interfaces/ISimpleSwapper.sol"; +import "./libraries/FullMath.sol"; /// @title PrivatePool /// @dev This contract allows contract calls to any contract (except BentoBox) @@ -276,50 +277,120 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 collateralShare = userCollateralShare[borrower]; if (collateralShare == 0) return false; - // Math overflow/edge case analysis + // The inequality that needs to hold for a user to be solvent is: // - // LHS: + // (value of user collateral) * (max LTV) >= (value of user debt) // - // collateralShare: 128 bits (at most the total Bento SHARE balance) - // EXCHANGE_RATE_PRECISION / BPS: lg(1e14) < 47 bits - // COLLATERALIZATION_RATE_BPS: lg(1e4) < 14 bits (max 100% = 10k BPS) + // Our exchange rates give "collateral per ether of asset", so it makes + // sense to express the value in the collateral token. The calculation + // for the collateral value is: // - // Actually, the product of the latter two fits in 60 bits. - // So what we pass to `toAmount` is safe. In there it gets multiplied - // by the 128-bit total collateral balance. So we need + // BentoBox tokens + // value = shares * --------------- + // BentoBox shares // - // collateralShare * (total Bento TOKEN balance) + // where BentoBox tokens resp. shares refer to the balances for the + // collateral (ERC20) contract. For the debt we get: // - // to fit in 196 bits. (See BentoBox contract). This actually has a - // chance of overflowing; if someone were to use the entire supply of - // SPELL as collateral, then 255 bits are used in `toAmount`.j + // Total debt tokens Collateral wei per asset ether + // value = parts * ----------------- * ------------------------------ + // Total debt parts 1e18 // - // Be careful of tokens with ridiculously high balances as collateral. - // SPELL is still OK, but even then the BentoBox can't lose too much in - // a strategy: we multiply SHARES by TOKENS at some point, and in - // theory the share/token ratio can swing arbitrarily high toward - // shares. + // ..since "tokens" is in wei. We call this EXCHANGE_RATE_PRECISION. + // Finally, max LTV is // - // This happens if the BentoBox loses tokens in a strategy, and then - // receives new tokens in a deposit. Those would then represent a - // large multiple of the current reserves, and thus be alotted a large - // share balance. + // COLLATERALIZATION_RATE_BPS + // ratio = --------------------------. + // 1e4 // + // We use the below table to (fit the inequality in 80 characters and) + // move some things around and justify our calculations. + // + // Description Variable in contract Bits + // ----------------------------------------------------------------- + // cs shares collateralShare 128* + // Be BentoBox tokens bentoBoxTotals.elastic 128 + // Bb BentoBox shares bentoBoxTotals.base 128 + // ----------------------------------------------------------------- + // dp parts debtPart 128* + // De Total debt tokens totalDebt.elastic 128 + // Db Total debt parts totalDebt.base 128 + // xr Coll. wei per asset ether exchangeRate 256 + // 1e18 EXCHANGE_RATE_PRECISION 60 + // ---------------------------------------------------------------- + // ltv COLLATERALIZATION_RATE_BPS 14 + // 1e4 BPS 14 + // 1e14 47 + // + // (* as in other proofs, these values fit in some 128-bit total). + // The inequality, without regard for integer division: + // + // Be ltv De xr + // cs * -- * --- >= dp * -- * ---- + // Bb 1e4 Db 1e18 + // + // This is equivalent to: + // + // cs * Be * ltv * Db * 1e14 >= dp * De * xr * Bb + // + // Corresponding bit counts: + // + // 128 + 128 + 14 + 128 + 47; 128 + 128 + 256 + 128 + // + // So, the LHS definitely fits in 512 bits, and as long as one token is + // not 2^68 times as valuable as another. Of course the other terms + // leave some leeway, too; if the exchange rate is this high, then the + // Bento share count is very unlikely to take up the full 128 bits. + // + // "Full" multiplication is not very expensive or cumbersome; the case + // where `exchangeRate` is too big is a little more involved, but + // manageable. Rebase memory _totalDebt = totalDebt; + Rebase memory bentoBoxTotals = bentoBox.totals(collateral); - return - bentoBox.toAmount( - collateral, - collateralShare * - (EXCHANGE_RATE_PRECISION / BPS) * - accrueInfo.COLLATERALIZATION_RATE_BPS, - false - ) >= - // Moved exchangeRate here instead of dividing the other side to - // preserve more precision - debtPart.mul(_totalDebt.elastic).mul(_exchangeRate) / - _totalDebt.base; + (uint256 leftLo, uint256 leftHi) = FullMath.fullMul( + collateralShare * bentoBoxTotals.elastic, + // Cast needed to avoid uint128 overflow + uint256(accrueInfo.COLLATERALIZATION_RATE_BPS) * + _totalDebt.base * + 1e14 + ); + uint256 rightLo; + uint256 rightHi; + if (_exchangeRate <= type(uint128).max) { + (rightLo, rightHi) = FullMath.fullMul( + debtPart * _totalDebt.elastic, + _exchangeRate * bentoBoxTotals.base + ); + } else { + // We multiply it out in stages to be safe. If the total overflows + // 512 bits then we are done, as the LHS is guaranteed to be less. + // + // aHi * 2^256 + aLo = dp * De * Bb + // ----------------------------------------------------------------- + // The total is then the sum of: + // + // bHi * 2^512 + bLo * 2^256 = xr * aHi * 2^256 + // cHi * 2^256 + cLo = xr * aLo + // + (uint256 aLo, uint256 aHi) = FullMath.fullMul( + debtPart * _totalDebt.elastic, + bentoBoxTotals.base + ); + (uint256 bLo, uint256 bHi) = FullMath.fullMul(_exchangeRate, aHi); + if (bHi > 0) { + return false; + } + + uint256 cHi; // (cLo is rightLo) + (rightLo, cHi) = FullMath.fullMul(_exchangeRate, aLo); + rightHi = cHi + bLo; + if (rightHi < cHi) { + return false; + } + } + return leftHi > rightHi || (leftHi == rightHi && leftLo >= rightLo); } /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body. @@ -540,10 +611,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { // BentoBox shares total if the transfer succeeds. But then "amount" // must fit in the token total; at least up to some rounding error, // which cannot be more than 128 bits either. - uint256 openFeeAmount = amount * _accrueInfo.BORROW_OPENING_FEE_BPS / + uint256 openFeeAmount = (amount * _accrueInfo.BORROW_OPENING_FEE_BPS) / BPS; // No overflow: Same reason. Also, we just divided by BPS.. - uint256 protocolFeeAmount = openFeeAmount * PROTOCOL_FEE_BPS / BPS; + uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS; uint256 protocolFeeShare = bentoBoxTotals.toBase( protocolFeeAmount, false diff --git a/contracts/libraries/FullMath.sol b/contracts/libraries/FullMath.sol index 131d80b8..b03f9fec 100644 --- a/contracts/libraries/FullMath.sol +++ b/contracts/libraries/FullMath.sol @@ -6,7 +6,7 @@ pragma solidity 0.6.12; // taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1 // license is CC-BY-4.0 library FullMath { - function fullMul(uint256 x, uint256 y) private pure returns (uint256 l, uint256 h) { + function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) { uint256 mm = mulmod(x, y, uint256(-1)); l = x * y; h = mm - l; diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index d7a1a07f..135f9339 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -812,9 +812,10 @@ describe("Private Lending Pool", async () => { makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true) ); + // This fails with the old Kashi-style `isSolvent`: it( - "Breaks at 393 billion (18-decimal) tokens", - makeIsSolventTest(getBigNumber(393_000_000_000n, 18), false) + "No longer breaks at 393 billion (18-decimal) tokens", + makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true) ); }); }); From f14bc0e043817cd8e6e6f52fa7f9d9921118877e Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 14 Dec 2021 15:59:55 +1100 Subject: [PATCH 011/107] Revert "(Archive oracles and swappers to silence warnings)" This reverts commit ae063b346f6b32ea09a4f1fe40154f64e9ccf7d6. --- {archive/contracts => contracts}/oracles/3CrvOracle.sol | 0 {archive/contracts => contracts}/oracles/3CryptoOracle.sol | 0 {archive/contracts => contracts}/oracles/AGLDUniV3Oracle.sol | 0 {archive/contracts => contracts}/oracles/ALCXOracle.sol | 0 {archive/contracts => contracts}/oracles/AVAXOracle.sol | 0 {archive/contracts => contracts}/oracles/AvaxLPOracle.sol | 0 {archive/contracts => contracts}/oracles/AvaxUsdtOracle.sol | 0 {archive/contracts => contracts}/oracles/BNBOracle.sol | 0 {archive/contracts => contracts}/oracles/BandOracleFTM.sol | 0 {archive/contracts => contracts}/oracles/CakeOracle.sol | 0 {archive/contracts => contracts}/oracles/ChainlinkOracle.sol | 0 {archive/contracts => contracts}/oracles/CompositeOracle.sol | 0 {archive/contracts => contracts}/oracles/CompoundOracle.sol | 0 {archive/contracts => contracts}/oracles/LPChainlinkOracle.sol | 0 {archive/contracts => contracts}/oracles/MimAvaxOracle.sol | 0 {archive/contracts => contracts}/oracles/PeggedOracle.sol | 0 {archive/contracts => contracts}/oracles/ProxyOracle.sol | 0 {archive/contracts => contracts}/oracles/RenBTCCrvOracle.sol | 0 {archive/contracts => contracts}/oracles/SimpleSLPTWAP0Oracle.sol | 0 {archive/contracts => contracts}/oracles/SimpleSLPTWAP1Oracle.sol | 0 {archive/contracts => contracts}/oracles/SpellOracle.sol | 0 {archive/contracts => contracts}/oracles/SpellTWAPOracle.sol | 0 {archive/contracts => contracts}/oracles/USTOracle.sol | 0 {archive/contracts => contracts}/oracles/YVCrvStETHOracle.sol | 0 {archive/contracts => contracts}/oracles/YVIronBankOracle.sol | 0 {archive/contracts => contracts}/oracles/YearnChainlinkOracle.sol | 0 {archive/contracts => contracts}/oracles/dQuickOracle.sol | 0 {archive/contracts => contracts}/oracles/sSpellOracle.sol | 0 {archive/contracts => contracts}/oracles/wMEMOOracle.sol | 0 {archive/contracts => contracts}/oracles/wOHMLinkOracle.sol | 0 {archive/contracts => contracts}/oracles/xJOEOracle.sol | 0 {archive/contracts => contracts}/oracles/xSUSHIOracle.sol | 0 .../contracts => contracts}/swappers/Leverage/AGLDLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/ALCXLevSwapper.sol | 0 .../swappers/Leverage/ArbEthLevSwapper.sol | 0 .../swappers/Leverage/AvaxUsdtLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/FTMLevSwapper.sol | 0 .../swappers/Leverage/MimAvaxLevSwapper.sol | 0 .../swappers/Leverage/RenCrvLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/SpellLevSwapper.sol | 0 .../swappers/Leverage/ThreeCryptoLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/USTLevSwapper.sol | 0 .../swappers/Leverage/UsdcAvaxLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/XJoeLevSwapper.sol | 0 .../swappers/Leverage/YVCrvStETHLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/YVIBLevSwapper.sol | 0 .../swappers/Leverage/YVUSDCLevSwapper.sol | 0 .../swappers/Leverage/YVUSDTLevSwapper.sol | 0 .../swappers/Leverage/YVWETHLevSwapper.sol | 0 .../swappers/Leverage/YVXSushiLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/YVYFILevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/wMemoLevSwapper.sol | 0 .../contracts => contracts}/swappers/Leverage/wOHMLevSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/AGLDSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/ALCXSwapper.sol | 0 .../swappers/Liquidations/ArbEthSwapper.sol | 0 .../swappers/Liquidations/AvaxUsdtSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/FTMSwapper.sol | 0 .../swappers/Liquidations/MimAvaxSwapper.sol | 0 .../swappers/Liquidations/RenCrvSwapper.sol | 0 .../swappers/Liquidations/SpellSuperSwapper.sol | 0 .../swappers/Liquidations/ThreeCryptoSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/USTSwapper.sol | 0 .../swappers/Liquidations/UsdcAvaxSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/XJoeSwapper.sol | 0 .../swappers/Liquidations/XSushiSwapper.sol | 0 .../swappers/Liquidations/YVCrvStETHSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/YVIBSwapper.sol | 0 .../swappers/Liquidations/YVUSDCSwapper.sol | 0 .../swappers/Liquidations/YVUSDTSwapper.sol | 0 .../swappers/Liquidations/YVWETHSwapper.sol | 0 .../swappers/Liquidations/YVYFISwapper.sol | 0 .../swappers/Liquidations/sSpellSwapper.sol | 0 .../swappers/Liquidations/wMEMOSwapper.sol | 0 .../contracts => contracts}/swappers/Liquidations/wOHMSwapper.sol | 0 {archive/contracts => contracts}/swappers/SpellSwapper.sol | 0 .../contracts => contracts}/swappers/SushiSwapMultiSwapper.sol | 0 {archive/contracts => contracts}/swappers/SushiSwapSwapper.sol | 0 78 files changed, 0 insertions(+), 0 deletions(-) rename {archive/contracts => contracts}/oracles/3CrvOracle.sol (100%) rename {archive/contracts => contracts}/oracles/3CryptoOracle.sol (100%) rename {archive/contracts => contracts}/oracles/AGLDUniV3Oracle.sol (100%) rename {archive/contracts => contracts}/oracles/ALCXOracle.sol (100%) rename {archive/contracts => contracts}/oracles/AVAXOracle.sol (100%) rename {archive/contracts => contracts}/oracles/AvaxLPOracle.sol (100%) rename {archive/contracts => contracts}/oracles/AvaxUsdtOracle.sol (100%) rename {archive/contracts => contracts}/oracles/BNBOracle.sol (100%) rename {archive/contracts => contracts}/oracles/BandOracleFTM.sol (100%) rename {archive/contracts => contracts}/oracles/CakeOracle.sol (100%) rename {archive/contracts => contracts}/oracles/ChainlinkOracle.sol (100%) rename {archive/contracts => contracts}/oracles/CompositeOracle.sol (100%) rename {archive/contracts => contracts}/oracles/CompoundOracle.sol (100%) rename {archive/contracts => contracts}/oracles/LPChainlinkOracle.sol (100%) rename {archive/contracts => contracts}/oracles/MimAvaxOracle.sol (100%) rename {archive/contracts => contracts}/oracles/PeggedOracle.sol (100%) rename {archive/contracts => contracts}/oracles/ProxyOracle.sol (100%) rename {archive/contracts => contracts}/oracles/RenBTCCrvOracle.sol (100%) rename {archive/contracts => contracts}/oracles/SimpleSLPTWAP0Oracle.sol (100%) rename {archive/contracts => contracts}/oracles/SimpleSLPTWAP1Oracle.sol (100%) rename {archive/contracts => contracts}/oracles/SpellOracle.sol (100%) rename {archive/contracts => contracts}/oracles/SpellTWAPOracle.sol (100%) rename {archive/contracts => contracts}/oracles/USTOracle.sol (100%) rename {archive/contracts => contracts}/oracles/YVCrvStETHOracle.sol (100%) rename {archive/contracts => contracts}/oracles/YVIronBankOracle.sol (100%) rename {archive/contracts => contracts}/oracles/YearnChainlinkOracle.sol (100%) rename {archive/contracts => contracts}/oracles/dQuickOracle.sol (100%) rename {archive/contracts => contracts}/oracles/sSpellOracle.sol (100%) rename {archive/contracts => contracts}/oracles/wMEMOOracle.sol (100%) rename {archive/contracts => contracts}/oracles/wOHMLinkOracle.sol (100%) rename {archive/contracts => contracts}/oracles/xJOEOracle.sol (100%) rename {archive/contracts => contracts}/oracles/xSUSHIOracle.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/AGLDLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/ALCXLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/ArbEthLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/AvaxUsdtLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/FTMLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/MimAvaxLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/RenCrvLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/SpellLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/ThreeCryptoLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/USTLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/UsdcAvaxLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/XJoeLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVCrvStETHLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVIBLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVUSDCLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVUSDTLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVWETHLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVXSushiLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/YVYFILevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/wMemoLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Leverage/wOHMLevSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/AGLDSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/ALCXSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/ArbEthSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/AvaxUsdtSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/FTMSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/MimAvaxSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/RenCrvSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/SpellSuperSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/ThreeCryptoSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/USTSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/UsdcAvaxSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/XJoeSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/XSushiSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVCrvStETHSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVIBSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVUSDCSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVUSDTSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVWETHSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/YVYFISwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/sSpellSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/wMEMOSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/Liquidations/wOHMSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/SpellSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/SushiSwapMultiSwapper.sol (100%) rename {archive/contracts => contracts}/swappers/SushiSwapSwapper.sol (100%) diff --git a/archive/contracts/oracles/3CrvOracle.sol b/contracts/oracles/3CrvOracle.sol similarity index 100% rename from archive/contracts/oracles/3CrvOracle.sol rename to contracts/oracles/3CrvOracle.sol diff --git a/archive/contracts/oracles/3CryptoOracle.sol b/contracts/oracles/3CryptoOracle.sol similarity index 100% rename from archive/contracts/oracles/3CryptoOracle.sol rename to contracts/oracles/3CryptoOracle.sol diff --git a/archive/contracts/oracles/AGLDUniV3Oracle.sol b/contracts/oracles/AGLDUniV3Oracle.sol similarity index 100% rename from archive/contracts/oracles/AGLDUniV3Oracle.sol rename to contracts/oracles/AGLDUniV3Oracle.sol diff --git a/archive/contracts/oracles/ALCXOracle.sol b/contracts/oracles/ALCXOracle.sol similarity index 100% rename from archive/contracts/oracles/ALCXOracle.sol rename to contracts/oracles/ALCXOracle.sol diff --git a/archive/contracts/oracles/AVAXOracle.sol b/contracts/oracles/AVAXOracle.sol similarity index 100% rename from archive/contracts/oracles/AVAXOracle.sol rename to contracts/oracles/AVAXOracle.sol diff --git a/archive/contracts/oracles/AvaxLPOracle.sol b/contracts/oracles/AvaxLPOracle.sol similarity index 100% rename from archive/contracts/oracles/AvaxLPOracle.sol rename to contracts/oracles/AvaxLPOracle.sol diff --git a/archive/contracts/oracles/AvaxUsdtOracle.sol b/contracts/oracles/AvaxUsdtOracle.sol similarity index 100% rename from archive/contracts/oracles/AvaxUsdtOracle.sol rename to contracts/oracles/AvaxUsdtOracle.sol diff --git a/archive/contracts/oracles/BNBOracle.sol b/contracts/oracles/BNBOracle.sol similarity index 100% rename from archive/contracts/oracles/BNBOracle.sol rename to contracts/oracles/BNBOracle.sol diff --git a/archive/contracts/oracles/BandOracleFTM.sol b/contracts/oracles/BandOracleFTM.sol similarity index 100% rename from archive/contracts/oracles/BandOracleFTM.sol rename to contracts/oracles/BandOracleFTM.sol diff --git a/archive/contracts/oracles/CakeOracle.sol b/contracts/oracles/CakeOracle.sol similarity index 100% rename from archive/contracts/oracles/CakeOracle.sol rename to contracts/oracles/CakeOracle.sol diff --git a/archive/contracts/oracles/ChainlinkOracle.sol b/contracts/oracles/ChainlinkOracle.sol similarity index 100% rename from archive/contracts/oracles/ChainlinkOracle.sol rename to contracts/oracles/ChainlinkOracle.sol diff --git a/archive/contracts/oracles/CompositeOracle.sol b/contracts/oracles/CompositeOracle.sol similarity index 100% rename from archive/contracts/oracles/CompositeOracle.sol rename to contracts/oracles/CompositeOracle.sol diff --git a/archive/contracts/oracles/CompoundOracle.sol b/contracts/oracles/CompoundOracle.sol similarity index 100% rename from archive/contracts/oracles/CompoundOracle.sol rename to contracts/oracles/CompoundOracle.sol diff --git a/archive/contracts/oracles/LPChainlinkOracle.sol b/contracts/oracles/LPChainlinkOracle.sol similarity index 100% rename from archive/contracts/oracles/LPChainlinkOracle.sol rename to contracts/oracles/LPChainlinkOracle.sol diff --git a/archive/contracts/oracles/MimAvaxOracle.sol b/contracts/oracles/MimAvaxOracle.sol similarity index 100% rename from archive/contracts/oracles/MimAvaxOracle.sol rename to contracts/oracles/MimAvaxOracle.sol diff --git a/archive/contracts/oracles/PeggedOracle.sol b/contracts/oracles/PeggedOracle.sol similarity index 100% rename from archive/contracts/oracles/PeggedOracle.sol rename to contracts/oracles/PeggedOracle.sol diff --git a/archive/contracts/oracles/ProxyOracle.sol b/contracts/oracles/ProxyOracle.sol similarity index 100% rename from archive/contracts/oracles/ProxyOracle.sol rename to contracts/oracles/ProxyOracle.sol diff --git a/archive/contracts/oracles/RenBTCCrvOracle.sol b/contracts/oracles/RenBTCCrvOracle.sol similarity index 100% rename from archive/contracts/oracles/RenBTCCrvOracle.sol rename to contracts/oracles/RenBTCCrvOracle.sol diff --git a/archive/contracts/oracles/SimpleSLPTWAP0Oracle.sol b/contracts/oracles/SimpleSLPTWAP0Oracle.sol similarity index 100% rename from archive/contracts/oracles/SimpleSLPTWAP0Oracle.sol rename to contracts/oracles/SimpleSLPTWAP0Oracle.sol diff --git a/archive/contracts/oracles/SimpleSLPTWAP1Oracle.sol b/contracts/oracles/SimpleSLPTWAP1Oracle.sol similarity index 100% rename from archive/contracts/oracles/SimpleSLPTWAP1Oracle.sol rename to contracts/oracles/SimpleSLPTWAP1Oracle.sol diff --git a/archive/contracts/oracles/SpellOracle.sol b/contracts/oracles/SpellOracle.sol similarity index 100% rename from archive/contracts/oracles/SpellOracle.sol rename to contracts/oracles/SpellOracle.sol diff --git a/archive/contracts/oracles/SpellTWAPOracle.sol b/contracts/oracles/SpellTWAPOracle.sol similarity index 100% rename from archive/contracts/oracles/SpellTWAPOracle.sol rename to contracts/oracles/SpellTWAPOracle.sol diff --git a/archive/contracts/oracles/USTOracle.sol b/contracts/oracles/USTOracle.sol similarity index 100% rename from archive/contracts/oracles/USTOracle.sol rename to contracts/oracles/USTOracle.sol diff --git a/archive/contracts/oracles/YVCrvStETHOracle.sol b/contracts/oracles/YVCrvStETHOracle.sol similarity index 100% rename from archive/contracts/oracles/YVCrvStETHOracle.sol rename to contracts/oracles/YVCrvStETHOracle.sol diff --git a/archive/contracts/oracles/YVIronBankOracle.sol b/contracts/oracles/YVIronBankOracle.sol similarity index 100% rename from archive/contracts/oracles/YVIronBankOracle.sol rename to contracts/oracles/YVIronBankOracle.sol diff --git a/archive/contracts/oracles/YearnChainlinkOracle.sol b/contracts/oracles/YearnChainlinkOracle.sol similarity index 100% rename from archive/contracts/oracles/YearnChainlinkOracle.sol rename to contracts/oracles/YearnChainlinkOracle.sol diff --git a/archive/contracts/oracles/dQuickOracle.sol b/contracts/oracles/dQuickOracle.sol similarity index 100% rename from archive/contracts/oracles/dQuickOracle.sol rename to contracts/oracles/dQuickOracle.sol diff --git a/archive/contracts/oracles/sSpellOracle.sol b/contracts/oracles/sSpellOracle.sol similarity index 100% rename from archive/contracts/oracles/sSpellOracle.sol rename to contracts/oracles/sSpellOracle.sol diff --git a/archive/contracts/oracles/wMEMOOracle.sol b/contracts/oracles/wMEMOOracle.sol similarity index 100% rename from archive/contracts/oracles/wMEMOOracle.sol rename to contracts/oracles/wMEMOOracle.sol diff --git a/archive/contracts/oracles/wOHMLinkOracle.sol b/contracts/oracles/wOHMLinkOracle.sol similarity index 100% rename from archive/contracts/oracles/wOHMLinkOracle.sol rename to contracts/oracles/wOHMLinkOracle.sol diff --git a/archive/contracts/oracles/xJOEOracle.sol b/contracts/oracles/xJOEOracle.sol similarity index 100% rename from archive/contracts/oracles/xJOEOracle.sol rename to contracts/oracles/xJOEOracle.sol diff --git a/archive/contracts/oracles/xSUSHIOracle.sol b/contracts/oracles/xSUSHIOracle.sol similarity index 100% rename from archive/contracts/oracles/xSUSHIOracle.sol rename to contracts/oracles/xSUSHIOracle.sol diff --git a/archive/contracts/swappers/Leverage/AGLDLevSwapper.sol b/contracts/swappers/Leverage/AGLDLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/AGLDLevSwapper.sol rename to contracts/swappers/Leverage/AGLDLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/ALCXLevSwapper.sol b/contracts/swappers/Leverage/ALCXLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/ALCXLevSwapper.sol rename to contracts/swappers/Leverage/ALCXLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/ArbEthLevSwapper.sol b/contracts/swappers/Leverage/ArbEthLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/ArbEthLevSwapper.sol rename to contracts/swappers/Leverage/ArbEthLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol b/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol rename to contracts/swappers/Leverage/AvaxUsdtLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/FTMLevSwapper.sol b/contracts/swappers/Leverage/FTMLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/FTMLevSwapper.sol rename to contracts/swappers/Leverage/FTMLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/MimAvaxLevSwapper.sol b/contracts/swappers/Leverage/MimAvaxLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/MimAvaxLevSwapper.sol rename to contracts/swappers/Leverage/MimAvaxLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/RenCrvLevSwapper.sol b/contracts/swappers/Leverage/RenCrvLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/RenCrvLevSwapper.sol rename to contracts/swappers/Leverage/RenCrvLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/SpellLevSwapper.sol b/contracts/swappers/Leverage/SpellLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/SpellLevSwapper.sol rename to contracts/swappers/Leverage/SpellLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol b/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol rename to contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/USTLevSwapper.sol b/contracts/swappers/Leverage/USTLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/USTLevSwapper.sol rename to contracts/swappers/Leverage/USTLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol b/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol rename to contracts/swappers/Leverage/UsdcAvaxLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/XJoeLevSwapper.sol b/contracts/swappers/Leverage/XJoeLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/XJoeLevSwapper.sol rename to contracts/swappers/Leverage/XJoeLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol b/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol rename to contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVIBLevSwapper.sol b/contracts/swappers/Leverage/YVIBLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVIBLevSwapper.sol rename to contracts/swappers/Leverage/YVIBLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVUSDCLevSwapper.sol b/contracts/swappers/Leverage/YVUSDCLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVUSDCLevSwapper.sol rename to contracts/swappers/Leverage/YVUSDCLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVUSDTLevSwapper.sol b/contracts/swappers/Leverage/YVUSDTLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVUSDTLevSwapper.sol rename to contracts/swappers/Leverage/YVUSDTLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVWETHLevSwapper.sol b/contracts/swappers/Leverage/YVWETHLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVWETHLevSwapper.sol rename to contracts/swappers/Leverage/YVWETHLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVXSushiLevSwapper.sol b/contracts/swappers/Leverage/YVXSushiLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVXSushiLevSwapper.sol rename to contracts/swappers/Leverage/YVXSushiLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/YVYFILevSwapper.sol b/contracts/swappers/Leverage/YVYFILevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/YVYFILevSwapper.sol rename to contracts/swappers/Leverage/YVYFILevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/wMemoLevSwapper.sol b/contracts/swappers/Leverage/wMemoLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/wMemoLevSwapper.sol rename to contracts/swappers/Leverage/wMemoLevSwapper.sol diff --git a/archive/contracts/swappers/Leverage/wOHMLevSwapper.sol b/contracts/swappers/Leverage/wOHMLevSwapper.sol similarity index 100% rename from archive/contracts/swappers/Leverage/wOHMLevSwapper.sol rename to contracts/swappers/Leverage/wOHMLevSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/AGLDSwapper.sol b/contracts/swappers/Liquidations/AGLDSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/AGLDSwapper.sol rename to contracts/swappers/Liquidations/AGLDSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/ALCXSwapper.sol b/contracts/swappers/Liquidations/ALCXSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/ALCXSwapper.sol rename to contracts/swappers/Liquidations/ALCXSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/ArbEthSwapper.sol b/contracts/swappers/Liquidations/ArbEthSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/ArbEthSwapper.sol rename to contracts/swappers/Liquidations/ArbEthSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol b/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/AvaxUsdtSwapper.sol rename to contracts/swappers/Liquidations/AvaxUsdtSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/FTMSwapper.sol b/contracts/swappers/Liquidations/FTMSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/FTMSwapper.sol rename to contracts/swappers/Liquidations/FTMSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/MimAvaxSwapper.sol b/contracts/swappers/Liquidations/MimAvaxSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/MimAvaxSwapper.sol rename to contracts/swappers/Liquidations/MimAvaxSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/RenCrvSwapper.sol b/contracts/swappers/Liquidations/RenCrvSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/RenCrvSwapper.sol rename to contracts/swappers/Liquidations/RenCrvSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/SpellSuperSwapper.sol b/contracts/swappers/Liquidations/SpellSuperSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/SpellSuperSwapper.sol rename to contracts/swappers/Liquidations/SpellSuperSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol b/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/ThreeCryptoSwapper.sol rename to contracts/swappers/Liquidations/ThreeCryptoSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/USTSwapper.sol b/contracts/swappers/Liquidations/USTSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/USTSwapper.sol rename to contracts/swappers/Liquidations/USTSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol b/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/UsdcAvaxSwapper.sol rename to contracts/swappers/Liquidations/UsdcAvaxSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/XJoeSwapper.sol b/contracts/swappers/Liquidations/XJoeSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/XJoeSwapper.sol rename to contracts/swappers/Liquidations/XJoeSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/XSushiSwapper.sol b/contracts/swappers/Liquidations/XSushiSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/XSushiSwapper.sol rename to contracts/swappers/Liquidations/XSushiSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol b/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVCrvStETHSwapper.sol rename to contracts/swappers/Liquidations/YVCrvStETHSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVIBSwapper.sol b/contracts/swappers/Liquidations/YVIBSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVIBSwapper.sol rename to contracts/swappers/Liquidations/YVIBSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVUSDCSwapper.sol b/contracts/swappers/Liquidations/YVUSDCSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVUSDCSwapper.sol rename to contracts/swappers/Liquidations/YVUSDCSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVUSDTSwapper.sol b/contracts/swappers/Liquidations/YVUSDTSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVUSDTSwapper.sol rename to contracts/swappers/Liquidations/YVUSDTSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVWETHSwapper.sol b/contracts/swappers/Liquidations/YVWETHSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVWETHSwapper.sol rename to contracts/swappers/Liquidations/YVWETHSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/YVYFISwapper.sol b/contracts/swappers/Liquidations/YVYFISwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/YVYFISwapper.sol rename to contracts/swappers/Liquidations/YVYFISwapper.sol diff --git a/archive/contracts/swappers/Liquidations/sSpellSwapper.sol b/contracts/swappers/Liquidations/sSpellSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/sSpellSwapper.sol rename to contracts/swappers/Liquidations/sSpellSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/wMEMOSwapper.sol b/contracts/swappers/Liquidations/wMEMOSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/wMEMOSwapper.sol rename to contracts/swappers/Liquidations/wMEMOSwapper.sol diff --git a/archive/contracts/swappers/Liquidations/wOHMSwapper.sol b/contracts/swappers/Liquidations/wOHMSwapper.sol similarity index 100% rename from archive/contracts/swappers/Liquidations/wOHMSwapper.sol rename to contracts/swappers/Liquidations/wOHMSwapper.sol diff --git a/archive/contracts/swappers/SpellSwapper.sol b/contracts/swappers/SpellSwapper.sol similarity index 100% rename from archive/contracts/swappers/SpellSwapper.sol rename to contracts/swappers/SpellSwapper.sol diff --git a/archive/contracts/swappers/SushiSwapMultiSwapper.sol b/contracts/swappers/SushiSwapMultiSwapper.sol similarity index 100% rename from archive/contracts/swappers/SushiSwapMultiSwapper.sol rename to contracts/swappers/SushiSwapMultiSwapper.sol diff --git a/archive/contracts/swappers/SushiSwapSwapper.sol b/contracts/swappers/SushiSwapSwapper.sol similarity index 100% rename from archive/contracts/swappers/SushiSwapSwapper.sol rename to contracts/swappers/SushiSwapSwapper.sol From 8a9812bcfdde03bd2e8b534c9ab5556c47e905e8 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 16 Dec 2021 01:07:53 +1100 Subject: [PATCH 012/107] Change expiration logic: if set, everyone insolvent after --- contracts/PrivatePool.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index a9a2cf58..406ca534 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -130,7 +130,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { struct AccrueInfo { uint64 lastAccrued; uint64 INTEREST_PER_SECOND; // (in units of 1/10^18) - uint64 NO_LIQUIDATIONS_BEFORE; + uint64 EXPIRATION; uint16 COLLATERALIZATION_RATE_BPS; uint16 LIQUIDATION_MULTIPLIER_BPS; uint16 BORROW_OPENING_FEE_BPS; @@ -140,8 +140,6 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 private constant PROTOCOL_FEE_BPS = 1000; // 10% uint256 private constant BPS = 10_000; - - // Must be well over BPS due to optimization in math: uint256 private constant EXCHANGE_RATE_PRECISION = 1e18; /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`. @@ -158,7 +156,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { address lender; address[] borrowers; uint64 INTEREST_PER_SECOND; - uint64 NO_LIQUIDATIONS_BEFORE; + uint64 EXPIRATION; uint16 COLLATERALIZATION_RATE_BPS; uint16 LIQUIDATION_MULTIPLIER_BPS; uint16 BORROW_OPENING_FEE_BPS; @@ -194,7 +192,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { AccrueInfo memory _aI; _aI.INTEREST_PER_SECOND = settings.INTEREST_PER_SECOND; - _aI.NO_LIQUIDATIONS_BEFORE = settings.NO_LIQUIDATIONS_BEFORE; + _aI.EXPIRATION = settings.EXPIRATION; _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS; _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS; _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS; @@ -977,8 +975,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { AccrueInfo memory _accrueInfo = accrueInfo; require( - block.timestamp >= _accrueInfo.NO_LIQUIDATIONS_BEFORE, - "Non-liquidation period" + block.timestamp >= _accrueInfo.EXPIRATION, + "PrivatePool: no liquidation yet" ); uint256 allCollateralShare; @@ -988,7 +986,13 @@ contract PrivatePool is BoringOwnable, IMasterContract { Rebase memory bentoBoxTotals = bentoBox.totals(collateral); for (uint256 i = 0; i < borrowers.length; i++) { address borrower = borrowers[i]; - if (!_isSolvent(borrower, _exchangeRate)) { + // If we set an expiration at all, then by the above check it is + // now past and every borrower can be liquidated at the current + // price: + if ( + (_accrueInfo.EXPIRATION > 0) || + !_isSolvent(borrower, _exchangeRate) + ) { uint256 debtPart; { uint256 availableDebtPart = borrowerDebtPart[borrower]; From b3461a4686c5ff36427d9227a9095be08ef20857 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 12 Dec 2021 06:59:04 +1100 Subject: [PATCH 013/107] Test loan repayment and "fees owed" logic --- test/PrivatePool.test.ts | 236 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 234 insertions(+), 2 deletions(-) diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index 135f9339..abb5aa21 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -27,6 +27,8 @@ import { encodeInitData } from "./PrivatePool"; const { formatUnits } = ethers.utils; const { MaxUint256, AddressZero, HashZero } = ethers.constants; +const one = getBigNumber(1); + // Cook actions const Cook = { ACTION_ADD_ASSET: 1, @@ -709,8 +711,6 @@ describe("Private Lending Pool", async () => { await oracle.set(rate); await pairContract.updateExchangeRate(); - - // await pairContract.connect(carol).borrow(carol.address, getBigNumber(100)) }); it("Should let anyone with collateral remove it", async () => { @@ -740,6 +740,238 @@ describe("Private Lending Pool", async () => { // isSolvent are OK). }); + describeSnapshot("Repay", () => { + const collatAmount1 = getBigNumber(31_415926535_897932384n, 0); + const collatShare1 = collatAmount1.mul(700).div(531); + + const collatAmount2 = getBigNumber(27_182818284_590452353n, 0); + const collatShare2 = collatAmount2.mul(700).div(531); + + const rate = getBigNumber(1, 18).div(12); // one WETH is 12 guineas + + const assetAmount = getBigNumber(400); + const assetShare = assetAmount.mul(9).div(20); + + const timeStep = 12345; + + const bobLoanAmount = assetAmount.mul(6).div(13); + + before(async () => { + await pairContract.connect(alice).addAsset(false, assetShare); + + const to1 = bob.address; + await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + + const to2 = carol.address; + await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + + await oracle.set(rate); + await pairContract.updateExchangeRate(); + + await pairContract.connect(bob).borrow(bob.address, bobLoanAmount); + }); + + it("Should let borrowers repay debt", async () => { + // const debt = assetAmount.div(2).mul( + // getBigNumber(1).add(MainTestSettings.INTEREST_PER_SECOND) + // ).div(getBigNumber(1)); + const timeStep = 12345; + + // As the first debtor, parts will be in 1-1 correspondence to amounts: + let debtPart = bobLoanAmount.add(bobLoanAmount.div(1000)); + let debtAmount = debtPart; + + let totalDebt = await pairContract.totalDebt(); + expect(totalDebt.elastic).to.equal(debtAmount); + expect(totalDebt.base).to.equal(debtPart); + + expect(await pairContract.borrowerDebtPart(bob.address)).to.equal( + debtPart + ); + + await advanceNextTime(timeStep); + const extraAmount = debtAmount + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(timeStep) + .div(one); + debtAmount = debtAmount.add(extraAmount); + + // "parts" are in units of the initial debt. These should cover it: + const repayPart = debtPart.div(4); + + // Bob owns all the debt, so this is the conversion. Rounding is up, in + // favour of the contract, so that the amount definitely covers the + // part intended to be paid back: + const repayAmount = repayPart + .mul(debtAmount) + .add(debtPart.sub(1)) + .div(debtPart); + + // "Smallest number of shares covering this" -- so rounded up again: + // Note that -- as in the UniV2 AMMs, for instance -- this number of + // shares could theoretically be used to cover a larger debt. + const repayShare = repayAmount.mul(9).add(19).div(20); + + const [g, b, p] = [guineas, bob, pairContract].map((x) => x.address); + expect( + await pairContract.connect(bob).repay(bob.address, false, repayPart) + ) + .to.emit(pairContract, "LogAccrue") + .withArgs(extraAmount, extraAmount.div(10)) + .to.emit(pairContract, "LogRepay") + .withArgs(bob.address, bob.address, repayAmount, repayPart) + .to.emit(bentoBox, "LogTransfer") + .withArgs(g, b, p, repayShare); + + debtPart = debtPart.sub(repayPart); + debtAmount = debtAmount.sub(repayAmount); + + totalDebt = await pairContract.totalDebt(); + + expect(totalDebt.elastic).to.equal(debtAmount); + expect(totalDebt.base).to.equal(debtPart); + }); + + it("Should use repayment towards fees owed, if any", async () => { + // In other words, the protocol gets first dibs on the fees. Fees are + // really owed by the lender, so from the borrower's POV nothing should + // change. + + // SETUP (and extra test for other calculations): + const timeStep = 234567; + + const bobLoanShare = bobLoanAmount.mul(9).div(20); + + const t0 = { + bobDebtAmount: bobLoanAmount.mul(1001).div(1000), + assetBalance: await pairContract.assetBalance(), + totalDebt: await pairContract.totalDebt(), + }; + t0.bobDebtPart = t0.bobDebtAmount; + + expect(t0.assetBalance.reservesShare).to.equal( + assetShare + .sub(bobLoanAmount.mul(9).div(20)) + .sub(bobLoanAmount.div(10_000).mul(9).div(20)) + ); + expect(t0.totalDebt.elastic).to.equal(t0.bobDebtAmount); + + const t1 = { + accruedInterest: t0.bobDebtAmount + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), + }; + // We find the number of shares Carol should borrow to drain reserves. We + // account for the protocol cut of Bob's accrued interest, and Carol's + // upcoming opening fee. Rounding up, because the calculation for the fee + // rounds down. This is not exact, because the contract starts with the + // amount, not the total. We deal with this by simply leaving a few wei + // of wiggle room (here and elsewhere); "multiplication and then division + // with rounding" is not an invertible operation, and not every target + // can be reached no matter how you round. (Try (M * 2) / 1 == 3). + const carolLoanShare = t0.assetBalance.reservesShare + .sub(t1.accruedInterest.div(10).mul(9).div(20)) + .add(1) + .mul(10_000) + .div(10_001); + const carolLoanAmount = carolLoanShare.mul(20).div(9).add(2); + + await advanceNextTime(timeStep); + await pairContract.connect(carol).borrow(carol.address, carolLoanAmount); + + t1.assetBalance = await pairContract.assetBalance(); + t1.feesOwedAmount = await pairContract.feesOwedAmount(); + expect(t1.assetBalance.reservesShare).to.be.lte(1); + expect(t1.feesOwedAmount).to.equal(0); + + // One accrual (after some time) should now be enough to cause fees to + // be owed. + + t1.totalDebt = await pairContract.totalDebt(); + + await advanceNextTime(timeStep); + await pairContract.accrue(); + + const t2 = { + accruedInterest: t1.totalDebt.elastic + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), + assetBalance: await pairContract.assetBalance(), + totalDebt: await pairContract.totalDebt(), + feesOwedAmount: await pairContract.feesOwedAmount(), + }; + // Since we have at most 1 wei in reserve, almost all of the protocol fee + // over the accrued interest will be "fees owed". + // The calculation involves two separate roundings and an addition, so + // the error is more than 1 or even `toAmount(1)`: + expect(t2.assetBalance.reservesShare).to.equal(0); + expect(t2.feesOwedAmount).to.be.gt(0); + expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte( + 5 + ); + + // Repaying triggers another accrual, so we (advance a fixed time and) + // determine how much will be owed after that: + const t3 = { + accruedInterest: t2.totalDebt.elastic + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), + }; + // Intermediate value; we would see it if we did something that accrues + // but not deposits any assets: + t3.feesOwedBeforeRepay = t2.feesOwedAmount.add( + t3.accruedInterest.div(10) + ); + + // A debt "part" corresponds to 1 token when the first loan is taken out. + // When interest accrues this amount grows correspondingly; it never + // shrinks. + // Repaying N "parts", then, definitely covers N tokens. Not by a lot; + // we've seen a bit over 1 week of 7%-a-year interest at this point. + // The amount we want to repay is the intermediate value of fees owed; + // after the accrual that gets triggered + const repayPart = t3.feesOwedBeforeRepay; + + await advanceNextTime(timeStep); + await pairContract.connect(bob).repay(bob.address, false, repayPart); + + t3.assetBalance = await pairContract.assetBalance(); + t3.totalDebt = await pairContract.totalDebt(); + t3.bobDebtPart = await pairContract.borrowerDebtPart(bob.address); + t3.feesOwedAmount = await pairContract.feesOwedAmount(); + + // Before the accrual, there were already fees owed, and therefore no + // asset reserves. More fees were then incurred. Since they could not be + // taken out of reserves, they were not "earned" until we made the + // repayment. At which point we expect "fees earned" to have increased + // by exactly that amount (in shares): + expect(t3.assetBalance.feesEarnedShare).to.equal( + t2.assetBalance.feesEarnedShare.add( + t3.feesOwedBeforeRepay.mul(9).div(20) + ) + ); + expect(t3.feesOwedAmount).to.equal(0); + + // Debt gets paid off as normal; in particular the fees are not added to + // it or anything: + expect(t3.bobDebtPart).to.equal(t0.bobDebtPart.sub(repayPart)); + expect(t3.totalDebt.base).to.equal(t2.totalDebt.base.sub(repayPart)); + + // The small amount we repaid in excess of the fees owed should have gone + // to asset reserves. + const excessRepayAmount = repayPart + .mul(t3.totalDebt.elastic) + .div(t3.totalDebt.base) + .sub(t3.feesOwedBeforeRepay); + const excessRepayShare = excessRepayAmount.mul(9).div(20); + + // Again, giving it a few wei of leeway due to rounding in _receiveAsset: + expect( + t3.assetBalance.reservesShare.sub(excessRepayShare).abs() + ).to.be.lte(5); + }); + }); + describeSnapshot("Edge Cases", () => { const makeIsSolventTest = (totalSupply, shouldPass) => async () => { const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); From 22b5f655db00759cc59b77411d4251c88ecb4840 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 17 Dec 2021 04:53:43 +1100 Subject: [PATCH 014/107] Remove some more overflow checks with proof --- contracts/PrivatePool.sol | 56 ++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 406ca534..45b3dabc 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -999,18 +999,25 @@ contract PrivatePool is BoringOwnable, IMasterContract { debtPart = maxDebtParts[i] > availableDebtPart ? availableDebtPart : maxDebtParts[i]; - borrowerDebtPart[borrower] = availableDebtPart.sub( - debtPart - ); + // No underflow: ensured by definition of debtPart + borrowerDebtPart[borrower] = availableDebtPart - debtPart; } uint256 debtAmount = _totalDebt.toElastic(debtPart, false); + // No overflow (inner): debtAmount <= totalDebt.elastic < 2^128. + // The exchange rate need not be reasonable: with an expiration + // time set there is no _isSolvent() call. uint256 collateralShare = bentoBoxTotals.toBase( - debtAmount.mul(_accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul( + (debtAmount * _accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul( _exchangeRate ) / (BPS * EXCHANGE_RATE_PRECISION), false ); + // This needs to be updated here so that the same user cannot + // be liquidated more than once. (Unless it adds up to one + // "full" liquidation or less). + // Underflow check is business logic: the liquidator can only + // take enough to cover the loan (and bonus). userCollateralShare[borrower] = userCollateralShare[borrower] .sub(collateralShare); @@ -1037,15 +1044,28 @@ contract PrivatePool is BoringOwnable, IMasterContract { ); } - // No overflow: subtracting from user balances succeeded - allCollateralShare = allCollateralShare.add(collateralShare); - allDebtAmount = allDebtAmount.add(debtAmount); - allDebtPart = allDebtPart.add(debtPart); + // No overflow in the below three: + // + // share(s) / amount(s) / part(s) involved in liquidation + // <= total share / amount / part + // <= (Bento).base / totalDebt.elastic / totalDebt.base + // < 2^128 + // + // Collateral share and debt part have already been + // successfully subtracted from some user's balance (and this + // persists across loop runs); the calculation for debt amount + // rounds down, so it fits if debtPart fits. It follows that + // the condition holds for the accumulated sums. + allCollateralShare += collateralShare; + allDebtAmount += debtAmount; + allDebtPart += debtPart; } } require(allDebtAmount != 0, "PrivatePool: all are solvent"); - _totalDebt.elastic = _totalDebt.elastic.sub(allDebtAmount.to128()); - _totalDebt.base = _totalDebt.base.sub(allDebtPart.to128()); + // No overflow (both): (liquidated debt) <= (total debt). + // Cast is safe (both): (liquidated debt) <= (total debt) < 2^128 + _totalDebt.elastic -= uint128(allDebtAmount); + _totalDebt.base -= uint128(allDebtPart); totalDebt = _totalDebt; if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) { @@ -1062,7 +1082,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { { CollateralBalance memory _collateralBalance = collateralBalance; - // No underflow: All amounts fit in the collateral BentoBox total + // No underflow: All amounts fit in the collateral Bento total _collateralBalance.userTotalShare -= uint128(excessShare); _collateralBalance.feesEarnedShare += uint128(feeShare); collateralBalance = _collateralBalance; @@ -1075,13 +1095,19 @@ contract PrivatePool is BoringOwnable, IMasterContract { excessShare - feeShare ); } else { - // No underflow: summands fit in user balances + // No underflow: allCollateralShare is the sum of quantities that + // have successfully been taken out of user balances. + // Cast is safe: Above reason, and userTotalShare < 2^128 collateralBalance.userTotalShare -= uint128(allCollateralShare); // Charge the protocol fee over the excess. - uint256 feeAmount = (allDebtAmount.mul( - _accrueInfo.LIQUIDATION_MULTIPLIER_BPS - ) / BPS).sub(allDebtAmount).mul(PROTOCOL_FEE_BPS) / BPS; + // No overflow: + // allDebtAmount <= totalDebt.elastic < 2^128 (proof in loop) + // LIQUIDATION_MULTIPLIER_BPS < 2^16 + // PROTOCOL_FEE_BPS <= 10k < 2^14 (or we have bigger problems) + uint256 feeAmount = (allDebtAmount * + (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS) * + PROTOCOL_FEE_BPS) / (BPS * BPS); // Swap using a swapper freely chosen by the caller // Open (flash) liquidation: get proceeds first and provide the From 889d833e505536acceef998f3dc3fafc9c2653d7 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 19 Dec 2021 05:43:22 +1100 Subject: [PATCH 015/107] Fix in-kind liquidation bonus calculation --- contracts/PrivatePool.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 45b3dabc..a134b596 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -1072,10 +1072,14 @@ contract PrivatePool is BoringOwnable, IMasterContract { // As with normal liquidations, the liquidator gets the excess, the // protocol gets a cut of the excess, and the lender gets 100% of // the value of the loan. + // allCollateralShare already includes the bonus, which in turn + // includes the protocol fee. We round the bonus down to favor the + // lender, and the fee to favor the liquidator: // Math: All collateral fits in 128 bits (BentoBox), so the // multiplications are safe: uint256 excessShare = (allCollateralShare * - (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / BPS; + (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / + _accrueInfo.LIQUIDATION_MULTIPLIER_BPS; uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS; uint256 lenderShare = allCollateralShare - excessShare; // (Stack depth): liquidatorShare = excessShare - feeShare; From 643a85a76073cd3d7af7f23549f724af3764290c Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 17 Dec 2021 06:13:38 +1100 Subject: [PATCH 016/107] Test regular and in-kind liquidations --- test/PrivatePool.test.ts | 649 +++++++++++++++++++++++++++++++-------- 1 file changed, 517 insertions(+), 132 deletions(-) diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index abb5aa21..73039422 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -72,7 +72,7 @@ describe("Private Lending Pool", async () => { let bentoBox: BentoBoxMock; let oracle: OracleMock; let masterContract: PrivatePool; - let pairContract: PrivatePool; + let mainPair: PrivatePool; let alice: Signer; let bob: Signer; let carol: Signer; @@ -220,7 +220,7 @@ describe("Private Lending Pool", async () => { // Guineas: 7000.0 // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) - pairContract = await deployPair({ + mainPair = await deployPair({ lender: alice.address, borrowers: [bob.address, carol.address], asset: guineas.address, @@ -232,13 +232,11 @@ describe("Private Lending Pool", async () => { describe("Deploy", async () => { it("Should deploy", async () => { - expect(await pairContract.lender()).to.equal(alice.address); + expect(await mainPair.lender()).to.equal(alice.address); for (const { address } of [carol, bob]) { - expect(await pairContract.approvedBorrowers(address)).to.equal(true); + expect(await mainPair.approvedBorrowers(address)).to.equal(true); } - expect(await pairContract.approvedBorrowers(alice.address)).to.equal( - false - ); + expect(await mainPair.approvedBorrowers(alice.address)).to.equal(false); }); it("Should reject bad settings", async () => { @@ -265,7 +263,7 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( + await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith( "PrivatePool: already initialized" ); }); @@ -274,13 +272,13 @@ describe("Private Lending Pool", async () => { describeSnapshot("Add Asset", async () => { it("Should let the lender add assets", async () => { const share = getBigNumber(450); - await expect(pairContract.connect(alice).addAsset(false, share)) - .to.emit(pairContract, "LogAddAsset") + await expect(mainPair.connect(alice).addAsset(false, share)) + .to.emit(mainPair, "LogAddAsset") .withArgs(alice.address, share) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, alice.address, pairContract.address, share); + .withArgs(guineas.address, alice.address, mainPair.address, share); - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); expect(assetBalance.feesEarnedShare).to.equal(0); }); @@ -288,14 +286,14 @@ describe("Private Lending Pool", async () => { it("Should let the lender add assets (skim)", async () => { // This is not a reasonable transaction.. const share = getBigNumber(450); - const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(pairContract.connect(alice).addAsset(true, share)) - .to.emit(pairContract, "LogAddAsset") + await expect(mainPair.connect(alice).addAsset(true, share)) + .to.emit(mainPair, "LogAddAsset") .withArgs(bentoBox.address, share); - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); expect(assetBalance.feesEarnedShare).to.equal(0); }); @@ -308,7 +306,7 @@ describe("Private Lending Pool", async () => { // This is what the BentoBox gives us for our deposit; round down: const share = (amount * 9n) / 20n; - const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; const datas = [ encodeParameters( @@ -322,47 +320,47 @@ describe("Private Lending Pool", async () => { // Make sure the existing Bento balance stays the same: const initialBentoBalance = await bentoBox.balanceOf(g, a); - await expect(pairContract.connect(alice).cook(actions, values, datas)) + await expect(mainPair.connect(alice).cook(actions, values, datas)) .to.emit(bentoBox, "LogDeposit") .withArgs(g, a, a, amount, share) - .to.emit(pairContract, "LogAddAsset") + .to.emit(mainPair, "LogAddAsset") .withArgs(a, share) .to.emit(bentoBox, "LogTransfer") .withArgs(g, a, p, share); expect(await bentoBox.balanceOf(g, a)).to.equal(initialBentoBalance); - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(share); expect(assetBalance.feesEarnedShare).to.equal(0); }); it("Should refuse to skim too much", async () => { const share = getBigNumber(123); - const [g, a, p] = [guineas, alice, pairContract].map((x) => x.address); + const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); await expect( - pairContract.connect(alice).addAsset(true, share.add(1)) + mainPair.connect(alice).addAsset(true, share.add(1)) ).to.be.revertedWith("PrivatePool: skim too much"); }); it("Should let anyone add assets", async () => { const share = getBigNumber(450); - await expect(pairContract.connect(bob).addAsset(false, share)) - .to.emit(pairContract, "LogAddAsset") + await expect(mainPair.connect(bob).addAsset(false, share)) + .to.emit(mainPair, "LogAddAsset") .withArgs(bob.address, share) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, bob.address, pairContract.address, share); + .withArgs(guineas.address, bob.address, mainPair.address, share); const share2 = 27_182_818_284_590_452_353n; - await expect(pairContract.connect(carol).addAsset(false, share2)) - .to.emit(pairContract, "LogAddAsset") + await expect(mainPair.connect(carol).addAsset(false, share2)) + .to.emit(mainPair, "LogAddAsset") .withArgs(carol.address, share2) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, carol.address, pairContract.address, share2); + .withArgs(guineas.address, carol.address, mainPair.address, share2); - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(share.add(share2)); expect(assetBalance.feesEarnedShare).to.equal(0); }); @@ -372,31 +370,29 @@ describe("Private Lending Pool", async () => { it("Should let approved borrowers add collateral", async () => { const share1 = getBigNumber(55); const to1 = bob.address; - await expect(pairContract.connect(bob).addCollateral(to1, false, share1)) - .to.emit(pairContract, "LogAddCollateral") + await expect(mainPair.connect(bob).addCollateral(to1, false, share1)) + .to.emit(mainPair, "LogAddCollateral") .withArgs(bob.address, to1, share1) .to.emit(bentoBox, "LogTransfer") - .withArgs(weth.address, bob.address, pairContract.address, share1); + .withArgs(weth.address, bob.address, mainPair.address, share1); - expect(await pairContract.userCollateralShare(to1)).to.equal(share1); + expect(await mainPair.userCollateralShare(to1)).to.equal(share1); - let collateralBalance = await pairContract.collateralBalance(); + let collateralBalance = await mainPair.collateralBalance(); expect(collateralBalance.userTotalShare).to.equal(share1); expect(collateralBalance.feesEarnedShare).to.equal(0); const share2 = 27_182_818_284_590_452_353n; const to2 = carol.address; - await expect( - pairContract.connect(carol).addCollateral(to2, false, share2) - ) - .to.emit(pairContract, "LogAddCollateral") + await expect(mainPair.connect(carol).addCollateral(to2, false, share2)) + .to.emit(mainPair, "LogAddCollateral") .withArgs(carol.address, to2, share2) .to.emit(bentoBox, "LogTransfer") - .withArgs(weth.address, carol.address, pairContract.address, share2); + .withArgs(weth.address, carol.address, mainPair.address, share2); - expect(await pairContract.userCollateralShare(to2)).to.equal(share2); + expect(await mainPair.userCollateralShare(to2)).to.equal(share2); - collateralBalance = await pairContract.collateralBalance(); + collateralBalance = await mainPair.collateralBalance(); expect(collateralBalance.userTotalShare).to.equal(share1.add(share2)); expect(collateralBalance.feesEarnedShare).to.equal(0); }); @@ -404,29 +400,29 @@ describe("Private Lending Pool", async () => { it("Should let anyone add collateral for approved borrowers", async () => { const share = getBigNumber(55); const to = bob.address; - await expect(pairContract.connect(alice).addCollateral(to, false, share)) - .to.emit(pairContract, "LogAddCollateral") + await expect(mainPair.connect(alice).addCollateral(to, false, share)) + .to.emit(mainPair, "LogAddCollateral") .withArgs(alice.address, to, share) .to.emit(bentoBox, "LogTransfer") - .withArgs(weth.address, alice.address, pairContract.address, share); + .withArgs(weth.address, alice.address, mainPair.address, share); }); it("Should refuse collateral for unapproved borrowers", async () => { const share = getBigNumber(55); const to = alice.address; await expect( - pairContract.connect(bob).addCollateral(to, false, share) + mainPair.connect(bob).addCollateral(to, false, share) ).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should let approved borrowers add collateral (skim)", async () => { const share = getBigNumber(55); const to = bob.address; - const [w, b, p] = [weth, bob, pairContract].map((x) => x.address); + const [w, b, p] = [weth, bob, mainPair].map((x) => x.address); await bentoBox.connect(bob).transfer(w, b, p, share); - await expect(pairContract.connect(bob).addCollateral(to, true, share)) - .to.emit(pairContract, "LogAddCollateral") + await expect(mainPair.connect(bob).addCollateral(to, true, share)) + .to.emit(mainPair, "LogAddCollateral") .withArgs(bentoBox.address, to, share); }); }); @@ -440,16 +436,16 @@ describe("Private Lending Pool", async () => { const ratePrecision = getBigNumber(1); before(async () => { - await pairContract.connect(alice).addAsset(false, assetShare); + await mainPair.connect(alice).addAsset(false, assetShare); const to1 = bob.address; - await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + await mainPair.connect(bob).addCollateral(to1, false, collatShare1); const to2 = carol.address; - await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + await mainPair.connect(carol).addCollateral(to2, false, collatShare2); await oracle.set(rate); - await pairContract.updateExchangeRate(); + await mainPair.updateExchangeRate(); }); it("Should allow approved borrowers to borrow", async () => { @@ -462,23 +458,23 @@ describe("Private Lending Pool", async () => { // Still 9 : 20 ratio const share = amount.mul(9).div(20); - const [g, b, p] = [guineas, bob, pairContract].map((x) => x.address); - await expect(pairContract.connect(bob).borrow(b, amount)) - .to.emit(pairContract, "LogBorrow") + const [g, b, p] = [guineas, bob, mainPair].map((x) => x.address); + await expect(mainPair.connect(bob).borrow(b, amount)) + .to.emit(mainPair, "LogBorrow") .withArgs(b, b, amount, fee, part) .to.emit(bentoBox, "LogTransfer") .withArgs(g, p, b, share); - const totalDebt = await pairContract.totalDebt(); + const totalDebt = await mainPair.totalDebt(); expect(totalDebt.elastic).to.equal(part); expect(totalDebt.base).to.equal(part); - expect(await pairContract.borrowerDebtPart(bob.address)).to.equal(part); + expect(await mainPair.borrowerDebtPart(bob.address)).to.equal(part); }); it("Should refuse to lend to unapproved borrowers", async () => { await expect( - pairContract.connect(alice).borrow(alice.address, 1) + mainPair.connect(alice).borrow(alice.address, 1) ).to.be.revertedWith("PrivatePool: unapproved borrower"); }); @@ -492,18 +488,19 @@ describe("Private Lending Pool", async () => { const borrowAmount = collatAmount.mul(9); // 75% of 12 await expect( - pairContract.connect(bob).borrow(bob.address, borrowAmount) + mainPair.connect(bob).borrow(bob.address, borrowAmount) ).to.be.revertedWith("PrivatePool: borrower insolvent"); // Accounting for the 0.1% open fee is enough to make it succeed: const withFee = borrowAmount.mul(1000).div(1001); - await expect( - pairContract.connect(bob).borrow(bob.address, withFee) - ).to.emit(pairContract, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit( + mainPair, + "LogBorrow" + ); // Borrowing even one more wei is enough to make it fail again: await expect( - pairContract.connect(bob).borrow(bob.address, withFee.add(1)) + mainPair.connect(bob).borrow(bob.address, withFee.add(1)) ).to.be.revertedWith("PrivatePool: borrower insolvent"); }); @@ -525,33 +522,33 @@ describe("Private Lending Pool", async () => { const takenShare = borrowShare.add(protocolFeeShare); await expect( - pairContract.connect(bob).borrow(bob.address, borrowAmount) - ).to.emit(pairContract, "LogBorrow"); + mainPair.connect(bob).borrow(bob.address, borrowAmount) + ).to.emit(mainPair, "LogBorrow"); - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(assetShare.sub(takenShare)); expect(assetBalance.feesEarnedShare).to.equal(protocolFeeShare); - expect(await pairContract.feesOwedAmount()).to.equal(0); + expect(await mainPair.feesOwedAmount()).to.equal(0); }); it("Should not lend out more than there is", async () => { // There are 1000 guineas of assets; 150 WETH allows for borrowing // 1350 and is therefore enough: - await pairContract + await mainPair .connect(bob) .addCollateral(bob.address, false, getBigNumber(150)); // More than reserves await expect( - pairContract.connect(bob).borrow(bob.address, getBigNumber(1001)) + mainPair.connect(bob).borrow(bob.address, getBigNumber(1001)) ).to.be.revertedWith("BoringMath: Underflow"); }); it("Should not defer the protocol fee on new loans", async () => { // This amounts to testing that the amount + protocol fee need to be in // reserve: - await pairContract + await mainPair .connect(bob) .addCollateral(bob.address, false, getBigNumber(150)); @@ -559,13 +556,14 @@ describe("Private Lending Pool", async () => { // not enough with the fee: const reservesAmount = getBigNumber(1000); await expect( - pairContract.connect(bob).borrow(bob.address, reservesAmount) + mainPair.connect(bob).borrow(bob.address, reservesAmount) ).to.be.revertedWith("BoringMath: Underflow"); const cutoff = reservesAmount.mul(1000).div(1001); - await expect( - pairContract.connect(bob).borrow(bob.address, cutoff) - ).to.emit(pairContract, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit( + mainPair, + "LogBorrow" + ); }); }); @@ -589,20 +587,20 @@ describe("Private Lending Pool", async () => { const YEAR = 3600 * 24 * 365; before(async () => { - await pairContract.connect(alice).addAsset(false, assetShare); + await mainPair.connect(alice).addAsset(false, assetShare); const to1 = bob.address; - await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + await mainPair.connect(bob).addCollateral(to1, false, collatShare1); const to2 = carol.address; - await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + await mainPair.connect(carol).addCollateral(to2, false, collatShare2); await oracle.set(rate); - await pairContract.updateExchangeRate(); + await mainPair.updateExchangeRate(); }); it("Should charge interest and collect fees over it", async () => { - await pairContract.connect(bob).borrow(bob.address, borrowAmount1); + await mainPair.connect(bob).borrow(bob.address, borrowAmount1); await advanceNextTime(YEAR); const perSecond = MainTestSettings.INTEREST_PER_SECOND; @@ -612,17 +610,17 @@ describe("Private Lending Pool", async () => { .div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(pairContract.accrue()) - .to.emit(pairContract, "LogAccrue") + await expect(mainPair.accrue()) + .to.emit(mainPair, "LogAccrue") .withArgs(extraAmount, feeAmount); // Protocol cut of the open fee + fee on interest, both in shares: - expect((await pairContract.assetBalance()).feesEarnedShare).to.equal( + expect((await mainPair.assetBalance()).feesEarnedShare).to.equal( openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20)) ); - expect(await pairContract.feesOwedAmount()).to.equal(0); + expect(await mainPair.feesOwedAmount()).to.equal(0); - const totalDebt = await pairContract.totalDebt(); + const totalDebt = await mainPair.totalDebt(); expect(totalDebt.base).to.equal(debtAmount1); expect(totalDebt.elastic).to.equal(debtAmount1.add(extraAmount)); @@ -636,7 +634,7 @@ describe("Private Lending Pool", async () => { }); it("Should not do anything if nothing is borrowed", async () => { - await pairContract.accrue(); + await mainPair.accrue(); // No "LogAccrue" event. Cleaner way to do this? expect( await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }]) @@ -655,7 +653,7 @@ describe("Private Lending Pool", async () => { .sub(openProtocolFeeShare); const initialDebt = almostEverything.add(openFee); - await pairContract.connect(bob).borrow(bob.address, almostEverything); + await mainPair.connect(bob).borrow(bob.address, almostEverything); const time = 1000 * YEAR; await advanceNextTime(time); @@ -667,18 +665,18 @@ describe("Private Lending Pool", async () => { .mul(time) .div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(pairContract.accrue()) - .to.emit(pairContract, "LogAccrue") + await expect(mainPair.accrue()) + .to.emit(mainPair, "LogAccrue") .withArgs(extraAmount, feeAmount); // Outstanding debt is recorded normally: - const totalDebt = await pairContract.totalDebt(); + const totalDebt = await mainPair.totalDebt(); expect(totalDebt.base).to.equal(initialDebt); expect(totalDebt.elastic).to.equal(initialDebt.add(extraAmount)); // Reserves are drained: what wasn't loaned out was collected as fees. // These already included the protocol fee: - const assetBalance = await pairContract.assetBalance(); + const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(0); expect(assetBalance.feesEarnedShare).to.equal( remainingShare.add(openProtocolFeeShare) @@ -688,7 +686,7 @@ describe("Private Lending Pool", async () => { const feeShare = feeAmount.mul(9).div(20); const stillOwedShare = feeShare.sub(remainingShare); const stillOwedAmount = stillOwedShare.mul(20).div(9); - expect(await pairContract.feesOwedAmount()).to.equal(stillOwedAmount); + expect(await mainPair.feesOwedAmount()).to.equal(stillOwedAmount); }); }); @@ -701,16 +699,16 @@ describe("Private Lending Pool", async () => { const ratePrecision = getBigNumber(1); before(async () => { - await pairContract.connect(alice).addAsset(false, assetShare); + await mainPair.connect(alice).addAsset(false, assetShare); const to1 = bob.address; - await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + await mainPair.connect(bob).addCollateral(to1, false, collatShare1); const to2 = carol.address; - await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + await mainPair.connect(carol).addCollateral(to2, false, collatShare2); await oracle.set(rate); - await pairContract.updateExchangeRate(); + await mainPair.updateExchangeRate(); }); it("Should let anyone with collateral remove it", async () => { @@ -719,20 +717,18 @@ describe("Private Lending Pool", async () => { // may change if we allow modifying the whitelist; then we'll have to // cleanly handle no-longer-whitelisted users. expect( - await pairContract - .connect(bob) - .removeCollateral(bob.address, collatShare1) + await mainPair.connect(bob).removeCollateral(bob.address, collatShare1) ) - .to.emit(pairContract, "LogRemoveCollateral") + .to.emit(mainPair, "LogRemoveCollateral") .withArgs(bob.address, bob.address, collatShare1); const remainder = getBigNumber(12); expect( - await pairContract + await mainPair .connect(carol) .removeCollateral(carol.address, collatShare2.sub(remainder)) ) - .to.emit(pairContract, "LogRemoveCollateral") + .to.emit(mainPair, "LogRemoveCollateral") .withArgs(carol.address, carol.address, collatShare2.sub(remainder)); }); @@ -757,18 +753,18 @@ describe("Private Lending Pool", async () => { const bobLoanAmount = assetAmount.mul(6).div(13); before(async () => { - await pairContract.connect(alice).addAsset(false, assetShare); + await mainPair.connect(alice).addAsset(false, assetShare); const to1 = bob.address; - await pairContract.connect(bob).addCollateral(to1, false, collatShare1); + await mainPair.connect(bob).addCollateral(to1, false, collatShare1); const to2 = carol.address; - await pairContract.connect(carol).addCollateral(to2, false, collatShare2); + await mainPair.connect(carol).addCollateral(to2, false, collatShare2); await oracle.set(rate); - await pairContract.updateExchangeRate(); + await mainPair.updateExchangeRate(); - await pairContract.connect(bob).borrow(bob.address, bobLoanAmount); + await mainPair.connect(bob).borrow(bob.address, bobLoanAmount); }); it("Should let borrowers repay debt", async () => { @@ -781,13 +777,11 @@ describe("Private Lending Pool", async () => { let debtPart = bobLoanAmount.add(bobLoanAmount.div(1000)); let debtAmount = debtPart; - let totalDebt = await pairContract.totalDebt(); + let totalDebt = await mainPair.totalDebt(); expect(totalDebt.elastic).to.equal(debtAmount); expect(totalDebt.base).to.equal(debtPart); - expect(await pairContract.borrowerDebtPart(bob.address)).to.equal( - debtPart - ); + expect(await mainPair.borrowerDebtPart(bob.address)).to.equal(debtPart); await advanceNextTime(timeStep); const extraAmount = debtAmount @@ -812,13 +806,11 @@ describe("Private Lending Pool", async () => { // shares could theoretically be used to cover a larger debt. const repayShare = repayAmount.mul(9).add(19).div(20); - const [g, b, p] = [guineas, bob, pairContract].map((x) => x.address); - expect( - await pairContract.connect(bob).repay(bob.address, false, repayPart) - ) - .to.emit(pairContract, "LogAccrue") + const [g, b, p] = [guineas, bob, mainPair].map((x) => x.address); + expect(await mainPair.connect(bob).repay(bob.address, false, repayPart)) + .to.emit(mainPair, "LogAccrue") .withArgs(extraAmount, extraAmount.div(10)) - .to.emit(pairContract, "LogRepay") + .to.emit(mainPair, "LogRepay") .withArgs(bob.address, bob.address, repayAmount, repayPart) .to.emit(bentoBox, "LogTransfer") .withArgs(g, b, p, repayShare); @@ -826,7 +818,7 @@ describe("Private Lending Pool", async () => { debtPart = debtPart.sub(repayPart); debtAmount = debtAmount.sub(repayAmount); - totalDebt = await pairContract.totalDebt(); + totalDebt = await mainPair.totalDebt(); expect(totalDebt.elastic).to.equal(debtAmount); expect(totalDebt.base).to.equal(debtPart); @@ -844,8 +836,8 @@ describe("Private Lending Pool", async () => { const t0 = { bobDebtAmount: bobLoanAmount.mul(1001).div(1000), - assetBalance: await pairContract.assetBalance(), - totalDebt: await pairContract.totalDebt(), + assetBalance: await mainPair.assetBalance(), + totalDebt: await mainPair.totalDebt(), }; t0.bobDebtPart = t0.bobDebtAmount; @@ -877,28 +869,28 @@ describe("Private Lending Pool", async () => { const carolLoanAmount = carolLoanShare.mul(20).div(9).add(2); await advanceNextTime(timeStep); - await pairContract.connect(carol).borrow(carol.address, carolLoanAmount); + await mainPair.connect(carol).borrow(carol.address, carolLoanAmount); - t1.assetBalance = await pairContract.assetBalance(); - t1.feesOwedAmount = await pairContract.feesOwedAmount(); + t1.assetBalance = await mainPair.assetBalance(); + t1.feesOwedAmount = await mainPair.feesOwedAmount(); expect(t1.assetBalance.reservesShare).to.be.lte(1); expect(t1.feesOwedAmount).to.equal(0); // One accrual (after some time) should now be enough to cause fees to // be owed. - t1.totalDebt = await pairContract.totalDebt(); + t1.totalDebt = await mainPair.totalDebt(); await advanceNextTime(timeStep); - await pairContract.accrue(); + await mainPair.accrue(); const t2 = { accruedInterest: t1.totalDebt.elastic .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) .div(one), - assetBalance: await pairContract.assetBalance(), - totalDebt: await pairContract.totalDebt(), - feesOwedAmount: await pairContract.feesOwedAmount(), + assetBalance: await mainPair.assetBalance(), + totalDebt: await mainPair.totalDebt(), + feesOwedAmount: await mainPair.feesOwedAmount(), }; // Since we have at most 1 wei in reserve, almost all of the protocol fee // over the accrued interest will be "fees owed". @@ -933,12 +925,12 @@ describe("Private Lending Pool", async () => { const repayPart = t3.feesOwedBeforeRepay; await advanceNextTime(timeStep); - await pairContract.connect(bob).repay(bob.address, false, repayPart); + await mainPair.connect(bob).repay(bob.address, false, repayPart); - t3.assetBalance = await pairContract.assetBalance(); - t3.totalDebt = await pairContract.totalDebt(); - t3.bobDebtPart = await pairContract.borrowerDebtPart(bob.address); - t3.feesOwedAmount = await pairContract.feesOwedAmount(); + t3.assetBalance = await mainPair.assetBalance(); + t3.totalDebt = await mainPair.totalDebt(); + t3.bobDebtPart = await mainPair.borrowerDebtPart(bob.address); + t3.feesOwedAmount = await mainPair.feesOwedAmount(); // Before the accrual, there were already fees owed, and therefore no // asset reserves. More fees were then incurred. Since they could not be @@ -972,6 +964,399 @@ describe("Private Lending Pool", async () => { }); }); + describeSnapshot("Liquidate -- normal", () => { + const bobCollateralAmount = getBigNumber(20); + const bobCollateralShare = bobCollateralAmount.mul(700).div(531); + + const carolCollateralAmount = getBigNumber(30); + const carolCollateralShare = carolCollateralAmount.mul(700).div(531); + + // Check that this covers rates we use.. + const assetAmount = getBigNumber(1000); + const assetShare = assetAmount.mul(9).div(20); + + const initialRate = one.div(10); // one WETH is 10 guineas + const bobLoanAmount = getBigNumber(100); // ~50% LTV + const carolLoanAmount = getBigNumber(100); // ~33% LTV + + const t0 = {}; + + before(async () => { + const [a, b, c] = [alice, bob, carol].map((x) => x.address); + await mainPair.connect(alice).addAsset(false, assetShare); + await mainPair.connect(bob).addCollateral(b, false, bobCollateralShare); + await mainPair + .connect(carol) + .addCollateral(c, false, carolCollateralShare); + + await oracle.set(one.div(10)); // one WETH is 10 guineas + await mainPair.updateExchangeRate(); + + await mainPair.connect(bob).borrow(b, bobLoanAmount); + await mainPair.connect(carol).borrow(c, carolLoanAmount); + + t0.aliceBentoGuineas = await bentoBox.balanceOf(guineas.address, a); + t0.aliceBentoWeth = await bentoBox.balanceOf(weth.address, a); + + t0.bobCollateralShare = await mainPair.userCollateralShare(b); + t0.carolCollateralShare = await mainPair.userCollateralShare(c); + + t0.bobDebtPart = await mainPair.borrowerDebtPart(b); + t0.carolDebtPart = await mainPair.borrowerDebtPart(c); + + t0.totalDebt = await mainPair.totalDebt(); + t0.assetBalance = await mainPair.assetBalance(); + t0.collateralBalance = await mainPair.collateralBalance(); + + t0.blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + }); + + it("Should refuse to liquidate solvent borrowers, at all", async () => { + // Not enough time will have passed to make either borrower insolvent + // over the accrued interest: + await expect( + mainPair.liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.be.revertedWith("PrivatePool: all are solvent"); + }); + + it("Should liquidate insolvent borrowers only", async () => { + const rate = one.div(5); + await oracle.set(rate); + await mainPair.updateExchangeRate(); + // Bob: ~100 / (20 * 5) ~= 100% => insolvent + // Carol: ~100 / (30 * 5) ~= 66% => solvent + // + const bobLiquidatePart = one; + const carolLiquidatePart = one; + + // Alice has guineas and has approved the contract. That she is also the + // lender makes no difference in the execution path taken. + await expect( + mainPair + .connect(alice) + .liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ) + .to.emit(mainPair, "LogRemoveCollateral") + .to.emit(mainPair, "LogRepay"); + + const t1 = { + totalDebt: await mainPair.totalDebt(), + bobDebtPart: await mainPair.borrowerDebtPart(bob.address), + carolDebtPart: await mainPair.borrowerDebtPart(carol.address), + + collateralBalance: await mainPair.collateralBalance(), + bobCollateralShare: await mainPair.userCollateralShare(bob.address), + carolCollateralShare: await mainPair.userCollateralShare(carol.address), + + assetBalance: await mainPair.assetBalance(), + + aliceBentoGuineas: await bentoBox.balanceOf( + guineas.address, + alice.address + ), + aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), + + blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, + }; + + // Amounts do not account for interest, but that should be very little + // Bob's "debt parts" correspond almost 1:1 to the amount of guineas he + // owes; the difference is interest accrued over a few blocks. + // This still gives us a firm lower bound on how much collateral gets + // taken from Bob: + const bobMinCollateralTakenShare = bobLiquidatePart + .mul(rate) + .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .div(one.mul(10_000)) + .mul(700) + .div(531); + + // Alice gets the collateral and bonus, in exchange for the loan amount + // plus the protocol fee on the bonus. + // While rounding (from amounts to shares) is an issue, the leeway is + // mostly to account for the interest, as in the collateral calculation. + const minRepayShare = bobLiquidatePart.mul(9).div(20); + // Not entirely accurate because it gets calculated differently, but + // equivalent up to rounding effects: + const protocolFeeShare = minRepayShare + .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .div(10_000) + .sub(minRepayShare) + .div(10); + const aliceMaxBentoGuineas = t0.aliceBentoGuineas + .sub(minRepayShare) + .sub(protocolFeeShare); + + expect(t1.aliceBentoGuineas).to.be.lte(aliceMaxBentoGuineas); + expect(t1.aliceBentoGuineas).to.be.gte( + aliceMaxBentoGuineas.mul(9999).div(10_000) + ); + + const aliceMinBentoWeth = t0.aliceBentoWeth.add( + bobMinCollateralTakenShare + ); + expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); + expect(t1.aliceBentoWeth).to.be.lte( + aliceMinBentoWeth.mul(10_001).div(10_000) + ); + + // If we want a firm lower bound on asset reserves, we need to account + // for interest: the accrue() call right before liquidations charges + // interest, and takes the protocol cut of that interest out of reserves. + // We divide rounding up: + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( + t1.blockTimestamp - t0.blockTimestamp + ) + .mul(t0.totalDebt.elastic) + .add(one.sub(1)) + .div(one) + .add(9) + .div(10); + const minAssetReserves = t0.assetBalance.reservesShare + .add(minRepayShare) + .sub(maxInterestFee); + const minFeesEarnedShare = + t0.assetBalance.feesEarnedShare.add(protocolFeeShare); + expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); + expect(t1.assetBalance.feesEarnedShare).to.be.gte(minFeesEarnedShare); + + // Carol was not insolvent, so that liquidation failed: + expect(t1.carolDebtPart).to.equal(t0.carolDebtPart); + expect(t1.carolCollateralShare).to.equal(t0.carolCollateralShare); + + // Bob got liquidated; this affects his balance and the totals: + expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); + expect(t1.totalDebt.base).to.equal( + t0.totalDebt.base.sub(bobLiquidatePart) + ); + + const bobMaxCollateralShare = t0.bobCollateralShare.sub( + bobMinCollateralTakenShare + ); + expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); + expect(t1.bobCollateralShare).to.be.gte( + bobMaxCollateralShare.mul(9999).div(10_000) + ); + // Equivalent check.. + expect(t1.collateralBalance.userTotalShare).to.equal( + t1.bobCollateralShare.add(t1.carolCollateralShare) + ); + }); + }); + + describeSnapshot("Liquidate -- in kind", () => { + const bobCollateralAmount = getBigNumber(20); + const bobCollateralShare = bobCollateralAmount.mul(700).div(531); + + const carolCollateralAmount = getBigNumber(30); + const carolCollateralShare = carolCollateralAmount.mul(700).div(531); + + // Check that this covers rates we use.. + const assetAmount = getBigNumber(1000); + const assetShare = assetAmount.mul(9).div(20); + + const initialRate = one.div(10); // one WETH is 10 guineas + const bobLoanAmount = getBigNumber(100); // ~50% LTV + const carolLoanAmount = getBigNumber(100); // ~33% LTV + + const t0 = {}; + let pair; + + before(async () => { + const [a, b, c] = [alice, bob, carol].map((x) => x.address); + + pair = await deployPair({ + lender: alice.address, + borrowers: [bob.address, carol.address], + asset: guineas.address, + collateral: weth.address, + oracle: oracle.address, + ...MainTestSettings, + LIQUIDATION_SEIZE_COLLATERAL: true, + }); + + await pair.connect(alice).addAsset(false, assetShare); + await pair.connect(bob).addCollateral(b, false, bobCollateralShare); + await pair.connect(carol).addCollateral(c, false, carolCollateralShare); + + await oracle.set(one.div(10)); // one WETH is 10 guineas + await pair.updateExchangeRate(); + + await pair.connect(bob).borrow(b, bobLoanAmount); + await pair.connect(carol).borrow(c, carolLoanAmount); + + t0.aliceBentoGuineas = await bentoBox.balanceOf(guineas.address, a); + t0.aliceBentoWeth = await bentoBox.balanceOf(weth.address, a); + + t0.bobCollateralShare = await pair.userCollateralShare(b); + t0.carolCollateralShare = await pair.userCollateralShare(c); + + t0.bobDebtPart = await pair.borrowerDebtPart(b); + t0.carolDebtPart = await pair.borrowerDebtPart(c); + + t0.totalDebt = await pair.totalDebt(); + t0.assetBalance = await pair.assetBalance(); + t0.collateralBalance = await pair.collateralBalance(); + + t0.blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + }); + + it("Should refuse to liquidate solvent borrowers, at all", async () => { + // Not enough time will have passed to make either borrower insolvent + // over the accrued interest: + await expect( + pair.liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.be.revertedWith("PrivatePool: all are solvent"); + }); + + it("Should liquidate insolvent borrowers only", async () => { + const rate = one.div(5); + await oracle.set(rate); + await pair.updateExchangeRate(); + // Bob: ~100 / (20 * 5) ~= 100% => insolvent + // Carol: ~100 / (30 * 5) ~= 66% => solvent + // + const bobLiquidatePart = one; + const carolLiquidatePart = one; + + // That Alice she is also the lender makes no difference in the execution + // path taken. + await expect( + pair + .connect(alice) + .liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.emit(pair, "LogSeizeCollateral"); + + const t1 = { + totalDebt: await pair.totalDebt(), + bobDebtPart: await pair.borrowerDebtPart(bob.address), + carolDebtPart: await pair.borrowerDebtPart(carol.address), + + aliceCollateralShare: await pair.userCollateralShare(alice.address), + bobCollateralShare: await pair.userCollateralShare(bob.address), + carolCollateralShare: await pair.userCollateralShare(carol.address), + + assetBalance: await pair.assetBalance(), + collateralBalance: await pair.collateralBalance(), + + aliceBentoGuineas: await bentoBox.balanceOf( + guineas.address, + alice.address + ), + aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), + + blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, + }; + + // Amounts do not account for interest, but that should be very little + // Bob's "debt parts" correspond almost 1:1 to the amount of guineas he + // owes; the difference is interest accrued over a few blocks. + // This still gives us a firm lower bound on how much collateral gets + // taken from Bob: + const bobMinCollateralTakenShare = bobLiquidatePart + .mul(rate) + .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .div(one.mul(10_000)) + .mul(700) + .div(531); + + // These need not add up (rounding), but should be firm lower bounds: + const minCollateralLiquidatorShare = bobMinCollateralTakenShare + .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS - 10_000) + .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .mul(9) + .div(10); + const minCollateralFeeShare = minCollateralLiquidatorShare.div(9); + const minCollateralLenderShare = bobMinCollateralTakenShare + .mul(10_000) + .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS); + + expect(t1.collateralBalance.feesEarnedShare).to.be.gte( + minCollateralFeeShare + ); + expect(t1.collateralBalance.feesEarnedShare).to.be.lte( + minCollateralFeeShare.mul(10_001).div(10_000) + ); + + // Alice gets the bonus only, in kind, minus the protocol fee. The + // contract gets the protocol fee over the bonus. + // No repayment: + expect(t1.aliceBentoGuineas).to.equal(t0.aliceBentoGuineas); + + // Alice the liquidator: + const aliceMinBentoWeth = t0.aliceBentoWeth.add( + minCollateralLiquidatorShare + ); + expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); + expect(t1.aliceBentoWeth).to.be.lte( + aliceMinBentoWeth.mul(10_001).div(10_000) + ); + + // Alice the lender: + expect(t1.aliceCollateralShare).to.be.gte(minCollateralLenderShare); + expect(t1.aliceCollateralShare).to.be.lte( + minCollateralLenderShare.mul(10_001).div(10_000) + ); + + // Asset reserves do not really change, except for the interest fee.. + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( + t1.blockTimestamp - t0.blockTimestamp + ) + .mul(t0.totalDebt.elastic) + .add(one.sub(1)) + .div(one) + .add(9) + .div(10); + const minAssetReserves = + t0.assetBalance.reservesShare.sub(maxInterestFee); + expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); + + // Carol was not insolvent, so that liquidation failed: + expect(t1.carolDebtPart).to.equal(t0.carolDebtPart); + expect(t1.carolCollateralShare).to.equal(t0.carolCollateralShare); + + // Bob got liquidated; this affects his balance and the totals: + expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); + expect(t1.totalDebt.base).to.equal( + t0.totalDebt.base.sub(bobLiquidatePart) + ); + + const bobMaxCollateralShare = t0.bobCollateralShare.sub( + bobMinCollateralTakenShare + ); + expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); + expect(t1.bobCollateralShare).to.be.gte( + bobMaxCollateralShare.mul(9999).div(10_000) + ); + // Given that individual shares are as expected, this tests the total: + expect(t1.collateralBalance.userTotalShare).to.equal( + t1.bobCollateralShare + .add(t1.carolCollateralShare) + .add(t1.aliceCollateralShare) + ); + }); + }); + describeSnapshot("Edge Cases", () => { const makeIsSolventTest = (totalSupply, shouldPass) => async () => { const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); From a7ffbaf982b0777b34362a673b9c20af31dd7670 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 21 Dec 2021 01:15:18 +1100 Subject: [PATCH 017/107] Start NFT-collateral contract (draft) Notable features/differences: - Same lender / borrowers model as PrivatePool - ERC-721 contract as collateral - One loan per contract token - Interest auto-compounds; no accrue() - Repayment can only be in full - Open fee comes out of principal - Lender can update loan parameters. If a loan is currently taken out then the update succeeds as long as it benefits the borrower. - No "flash borrows". (Would probably use a separate borrow function). - Expirations are required, and collateral gets auto-seized by the lender on expiration. Other than that, no liquidations. --- contracts/PrivatePoolNFT.sol | 774 +++++++++++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 5 + 3 files changed, 780 insertions(+) create mode 100644 contracts/PrivatePoolNFT.sol diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol new file mode 100644 index 00000000..bffb393c --- /dev/null +++ b/contracts/PrivatePoolNFT.sol @@ -0,0 +1,774 @@ +// SPDX-License-Identifier: UNLICENSED + +// Private Pool (NFT collateral) + +// ( ( ( +// )\ ) ( )\ )\ ) ( +// (((_) ( /( ))\ ((_)(()/( )( ( ( +// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ ) +// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/( +// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \)) +// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_| + +// Copyright (c) 2021 BoringCrypto - All rights reserved +// Twitter: @Boring_Crypto + +// Special thanks to: +// @0xKeno - for all his invaluable contributions +// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +/// @title PrivatePoolNFT +/// @dev This contract allows contract calls to any contract (except BentoBox) +/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. +contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { + using BoringMath for uint256; + using BoringMath128 for uint128; + using RebaseLibrary for Rebase; + using BoringERC20 for IERC20; + + event LogAddCollateral( + address indexed from, + address indexed to, + uint256 tokenId + ); + event LogAddAsset(address indexed from, uint256 share); + event LogRemoveCollateral( + address indexed from, + address indexed to, + uint256 tokenId + ); + event LogRemoveAsset(address indexed to, uint256 share); + event LogBorrow(address indexed from, address indexed to, uint256 tokenId); + event LogRepay(address indexed from, uint256 tokenId); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees(address indexed feeTo, uint256 feeShare); + + // Immutables (for MasterContract and all clones) + IBentoBoxV1 public immutable bentoBox; + PrivatePoolNFT public immutable masterContract; + + // MasterContract variables + address public feeTo; + + // Per clone variables + // Clone init settings + IERC721 public collateral; + IERC20 public asset; + + address public lender; + mapping(address => bool) public approvedBorrowers; + + // A note on terminology: + // "Shares" are BentoBox shares. + + // The BentoBox balance is the sum of the below two. + struct AssetBalance { + uint128 reservesShare; + uint128 feesEarnedShare; + } + AssetBalance public assetBalance; + + // Per token settings. + // We might allow the lender to update these, but only to the benefit of + // (potential) borrowers + struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 expiration; // Pay before this or get liquidated + uint16 openFeeBPS; // Fixed cost of taking out the loan + uint16 annualInterestBPS; // Variable cost of taking out the loan + } + mapping(uint256 => TokenLoanParams) public tokenLoanParams; + + uint8 private constant LOAN_INITIAL = 0; + uint8 private constant LOAN_COLLATERAL_DEPOSITED = 1; + uint8 private constant LOAN_TAKEN = 2; + struct TokenLoan { + address borrower; + uint64 startTime; + uint8 status; + } + mapping(uint256 => TokenLoan) public tokenLoan; + + // TODO: Configurable per loan? + uint256 private constant COMPOUND_INTEREST_TERMS = 4; // Do not go over 50. + uint256 private constant PROTOCOL_FEE_BPS = 1000; // Do not go over 100%.. + uint256 private constant BPS = 10_000; + uint256 private constant YEAR = 3600 * 24 * 365; + + /// @notice The constructor is only used for the initial master contract. + /// @notice Subsequent clones are initialised via `init`. + constructor(IBentoBoxV1 bentoBox_) public { + bentoBox = bentoBox_; + masterContract = this; + } + + struct InitSettings { + IERC721 collateral; + IERC20 asset; + address lender; + address[] borrowers; + uint256[] tokenIds; + TokenLoanParams[] loanParams; + } + + /// @notice De facto constructor for clone contracts + function init(bytes calldata data) public payable override { + require( + address(collateral) == address(0), + "PrivatePool: already initialized" + ); + + InitSettings memory settings = abi.decode(data, (InitSettings)); + require( + address(settings.collateral) != address(0), + "PrivatePool: bad pair" + ); + + collateral = settings.collateral; + asset = settings.asset; + lender = settings.lender; + + for (uint256 i = 0; i < settings.borrowers.length; i++) { + approvedBorrowers[settings.borrowers[i]] = true; + } + for (uint256 i = 0; i < settings.tokenIds.length; i++) { + _updateLoanParams(settings.tokenIds[i], settings.loanParams[i]); + } + } + + // Enforces that settings are valid + function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) + internal + { + require(params.openFeeBPS < BPS, "PrivatePool: open fee"); + tokenLoanParams[tokenId] = params; + } + + // Enforces that changes only benefit the borrower, if any. + // Can be changed, but only in favour of the borrower. This includes giving + // them another shot. + function updateLoanParams(uint256 tokenId, TokenLoanParams calldata params) + public + { + require(msg.sender == lender, "PrivatePool: not the lender"); + uint8 loanStatus = tokenLoan[tokenId].status; + if (loanStatus == LOAN_TAKEN) { + TokenLoanParams memory current = tokenLoanParams[tokenId]; + require( + params.expiration >= current.expiration && + params.valuation <= current.valuation && + params.annualInterestBPS <= current.annualInterestBPS, + "PrivatePool: worse params" + ); + } + _updateLoanParams(tokenId, params); + } + + /// @notice Adds `collateral` from msg.sender to the account `to`. + /// @param to The receiver of the tokens. + /// @param skim False if we need to transfer from msg.sender to the contract + /// @param tokenId The token to add as collateral + function addCollateral( + uint256 tokenId, + address to, + bool skim + ) public { + require(approvedBorrowers[to], "PrivatePool: unapproved borrower"); + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + require(loanParams.valuation > 0, "PrivatePool: loan unavailable"); + + if (skim) { + require( + collateral.ownerOf(tokenId) == address(this), + "PrivatePool: skim failed" + ); + require( + tokenLoan[tokenId].status == LOAN_INITIAL, + "PrivatePool: in use" + ); + } else { + collateral.safeTransferFrom(msg.sender, address(this), tokenId); + } + TokenLoan memory loan; + loan.borrower = to; + loan.status = LOAN_COLLATERAL_DEPOSITED; + tokenLoan[tokenId] = loan; + emit LogAddCollateral(skim ? address(this) : msg.sender, to, tokenId); + } + + // Equals to + // `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + // which can be also obtained as + // `IERC721Receiver(0).onERC721Received.selector` + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external override returns (bytes4) { + // We could check that this token can actually be used as collateral, + // but we leave that to the sender and save a little gas.. + return _ERC721_RECEIVED; + } + + /// @notice Removes `tokenId` as collateral and transfers it to `to`. + /// @param to The receiver of the token. + /// @param tokenId The token + function removeCollateral(uint256 tokenId, address to) public { + TokenLoan memory loan = tokenLoan[tokenId]; + if (msg.sender == loan.borrower) { + require( + loan.status == LOAN_COLLATERAL_DEPOSITED, + "PrivatePool: not paid off" + ); + } else { + // We are seizing collateral as the lender. The loan has to be + // expired and not paid off: + require(loan.status == LOAN_TAKEN, "PrivatePool: paid off"); + require(msg.sender == lender, "PrivatePool: not the lender"); + require( + tokenLoanParams[tokenId].expiration <= block.timestamp, + "PrivatePool: not expired" + ); + } + delete tokenLoan[tokenId]; + collateral.safeTransferFrom(address(this), to, tokenId); + emit LogRemoveCollateral(loan.borrower, to, tokenId); + } + + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// @param toReservesShare Amount of shares to reserves. + /// @param toReservesAmount Token amount. Ignored if `toReservesShare` nonzero. + /// @param toFeesAmount Token fee amount. Ignored if `toReservesShare` nonzero. + function _receiveAsset( + bool skim, + uint256 toReservesShare, + // (There is no case where we pass along a fee in shares) + uint256 toReservesAmount, + uint256 toFeesAmount + ) internal { + IERC20 _asset = asset; + AssetBalance memory _assetBalance = assetBalance; + uint256 priorAssetTotalShare = _assetBalance.reservesShare + + _assetBalance.feesEarnedShare; + Rebase memory bentoBoxTotals = bentoBox.totals(_asset); + + uint256 toFeesShare = 0; + if (toReservesShare == 0) { + toReservesShare = bentoBoxTotals.toBase(toReservesAmount, true); + if (toFeesAmount > 0) { + toFeesShare = bentoBoxTotals.toBase(toFeesAmount, false); + } + } + uint256 takenShare = toReservesShare.add(toFeesShare); + + // No overflow, cast safe: takenShare is bigger and fits in 128 bits if + // the transfer or skim succeeds + _assetBalance.reservesShare += uint128(toReservesShare); + _assetBalance.feesEarnedShare += uint128(toFeesShare); + assetBalance = _assetBalance; + + if (skim) { + require( + takenShare <= + bentoBox.balanceOf(_asset, address(this)).sub( + priorAssetTotalShare + ), + "PrivatePool: skim too much" + ); + } else { + bentoBox.transfer(_asset, msg.sender, address(this), takenShare); + } + } + + /// @notice Adds assets to the lending pair. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param share The amount of shares to add. + function addAsset(bool skim, uint256 share) public { + _receiveAsset(skim, share, 0, 0); + emit LogAddAsset(skim ? address(bentoBox) : msg.sender, share); + } + + /// @notice Removes an asset from msg.sender and transfers it to `to`. + /// @param to The address that receives the removed assets. + /// @param share The amount of shares to remove. + function removeAsset(address to, uint256 share) public { + require(msg.sender == lender, "PrivatePool: not the lender"); + // Cast safe: Bento transfer reverts unless stronger condition holds + assetBalance.reservesShare = assetBalance.reservesShare.sub( + uint128(share) + ); + bentoBox.transfer(asset, address(this), to, share); + emit LogRemoveAsset(to, share); + } + + /// Returns the Bento shares received. Passes the entire loan parameters as + /// a safeguard against these being "frontrun". + function borrow( + uint256 tokenId, + address to, + uint128 minValuation, + uint64 minExpiration, + uint16 maxOpenFeeBPS, + uint16 maxAnnualInterestBPS + ) public returns (uint256 share, uint256 amount) { + TokenLoan memory loan = tokenLoan[tokenId]; + // If you managed to add the collateral, then you are approved. (Even + // if we add a method to update the borrower whitelist later..) + require( + loan.status == LOAN_COLLATERAL_DEPOSITED && + loan.borrower == msg.sender, + "PrivatePool: no collateral" + ); + TokenLoanParams memory params = tokenLoanParams[tokenId]; + require(params.expiration > block.timestamp, "PrivatePool: expired"); + require( + params.valuation >= minValuation && + params.expiration >= minExpiration && + params.openFeeBPS <= maxOpenFeeBPS && + params.annualInterestBPS <= maxAnnualInterestBPS, + "PrivatePool: bad params" + ); + + IERC20 _asset = asset; + Rebase memory bentoBoxTotals = bentoBox.totals(_asset); + + uint256 protocolFeeShare; + { + // No overflow: max 128 + 16 bits + uint256 openFeeAmount = (uint256(params.valuation) * + params.openFeeBPS) / BPS; + // No overflow: max 144 + 16 bits + uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / + BPS; + // No underflow: openFeeBPS < BPS is enforced. + amount = params.valuation - openFeeAmount; + protocolFeeShare = bentoBoxTotals.toBase(protocolFeeAmount, false); + share = bentoBoxTotals.toBase(amount, false); + } + + { + AssetBalance memory _assetBalance = assetBalance; + // No overflow on the add: protocolFeeShare < share < Bento total, + // or the transfer reverts. The transfer is independent of the + // results of these calculations: `share` is not modified. + // Theoretically the fee could just make it overflow 128 bits. + // Underflow check is core business logic: + _assetBalance.reservesShare = _assetBalance.reservesShare.sub( + (share + protocolFeeShare).to128() + ); + // Cast is safe: `share` fits. Also, the checked cast above + // succeeded. No overflow: protocolFeeShare < reservesShare, and + // both balances together fit in the Bento share balance, + _assetBalance.feesEarnedShare += uint128(protocolFeeShare); + assetBalance = _assetBalance; + } + + loan.status = LOAN_TAKEN; + loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. + tokenLoan[tokenId] = loan; + bentoBox.transfer(_asset, address(this), to, share); + emit LogBorrow(msg.sender, to, tokenId); + } + + /// Approximates continuous compounding. Uses Horner's method to evaluate + /// the truncated Maclaurin series for exp - 1, accumulating rounding + /// errors along the way. The following is always guaranteed: + /// + /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), + /// + /// where time = t/YEAR, but once the result no longer fits in 128 bits it + /// may be very inaccurate. Which does not matter, because the BentoBox + /// cannot hold that high a balance. + /// + /// @param n Highest term. Set n=1 for linear (non-compound) interest. + function calculateInterest( + uint256 principal, + uint256 t, + uint256 aprBPS, + uint256 n + ) public pure returns (uint256 interest) { + // These calculations can, in principle, overflow, given sufficiently + // ridiculous inputs, as shown in the following table: + // + // principal = 2^128 - 1 (128 bits) + // t = 30,000 years (40 bits) + // interest = 655.35% APR (16 bits) + // + // Even then, we will not see an overflow until the fifth term: + // + // k denom > 2^ term * x <= 2^ term * x / denom <= 2^ + // --------------------------------------------------------------- + // 1 38 128 + 56 = 184 184 - 38 = 146 + // 2 39 146 + 56 = 202 202 - 39 = 163 + // 3 40 163 + 56 = 219 219 - 40 = 179 + // 4 42 179 + 56 = 235 235 - 42 = 193 + // 5 45 193 + 56 = 249 249 - 45 = 204 + // + // (Denominator bits: floor (lg (k! * 10_000 * YEAR)) ) + // + // To be fair, five terms would not adequately capture the effects of + // this much compound interest over this time period. On the high end + // of actual usage we expect to see, it does, and there is no overflow: + // + // principal = 1 billion ether (1e27) (90 bits) + // t = 5 years (~158 million seconds) (28 bits) + // apr = 30% (12 bits) + // + // k denom > 2^ term * x <= 2^ term * x / denom <= 2^ + // --------------------------------------------------------------- + // 1 38 90 + 40 = 130 130 - 38 = 92 + // 2 39 92 + 40 = 132 132 - 39 = 93 + // 3 40 93 + 40 = 133 133 - 40 = 93 + // 4 42 93 + 40 = 133 133 - 42 = 91 + // + // ..and from here on, the terms keep getting smaller; the factorial in + // the denominator "wins". Indeed, the result is dominated by the "2" + // and "3" terms: the partial sums are: + // + // n Σ_1..n (1.5^k / k!) + // -------------------------------------- + // 1 1.5 + // 2 2.625 + // 3 3.1875 + // 4 3.3984375 + // 5 3.46171875 + // ... + // (Infinity) 3.48168907... (e^1.5 - 1) + // + // Finally: the denominator overflows at n = 51; n = 50 is "safe" + // but useless; if we need that many terms, interest is high enough + // to be unpayable. + // However, n >= 252 is not safe; 10_000 * YEAR * 252! = 0 mod 2^256. + // + // Since even abnormal values will result in a few "valid" terms that + // are enough to make the interest unpayably high, it suffices to check + // that the total cannot go down (final `add`). If that calculation + // overflows, then reverting is no worse than anything else we may do. + // + uint256 x = t * aprBPS; + uint256 denom = YEAR * BPS; + uint256 term = (principal * x) / denom; + interest = term; + for (uint256 k = 2; k <= n; k++) { + term *= x; // Safe: See above. + denom *= k; // Fits up to k = 50; no problem after + term /= denom; // Safe until n = 251 + interest = interest.add(term); // <- Only overflow check we need + } + } + + function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_TAKEN, "PrivatePool: no loan"); + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + require( + loanParams.expiration > block.timestamp, + "PrivatePool: loan expired" + ); + + uint256 principal = loanParams.valuation; + + // No underflow: loan.startTime is only ever set to a block timestamp + uint256 interest = calculateInterest( + principal, + block.timestamp - loan.startTime, + loanParams.annualInterestBPS, + COMPOUND_INTEREST_TERMS + ).to128(); + // No overflow (both lines): to128() would have reverted + amount = principal + interest; + uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; + + // No underflow: PROTOCOL_FEE_BPS < BPS by construction. + _receiveAsset(skim, 0, amount - fee, fee); + loan.status = LOAN_COLLATERAL_DEPOSITED; + tokenLoan[tokenId] = loan; + emit LogRepay(skim ? address(bentoBox) : msg.sender, tokenId); + } + + uint8 internal constant ACTION_ADD_ASSET = 1; + uint8 internal constant ACTION_REPAY = 2; + uint8 internal constant ACTION_REMOVE_ASSET = 3; + uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; + uint8 internal constant ACTION_BORROW = 5; + + uint8 internal constant ACTION_ADD_COLLATERAL = 10; + + // Function on BentoBox + uint8 internal constant ACTION_BENTO_DEPOSIT = 20; + uint8 internal constant ACTION_BENTO_WITHDRAW = 21; + uint8 internal constant ACTION_BENTO_TRANSFER = 22; + uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; + uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; + + // Any external call (except to BentoBox) + uint8 internal constant ACTION_CALL = 30; + + int256 internal constant USE_VALUE1 = -1; + int256 internal constant USE_VALUE2 = -2; + + /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. + function _num( + int256 inNum, + uint256 value1, + uint256 value2 + ) internal pure returns (uint256 outNum) { + outNum = inNum >= 0 + ? uint256(inNum) + : (inNum == USE_VALUE1 ? value1 : value2); + } + + /// @dev Helper function for depositing into `bentoBox`. + function _bentoDeposit( + bytes memory data, + uint256 value, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode( + data, + (IERC20, address, int256, int256) + ); + amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors + share = int256(_num(share, value1, value2)); + return + bentoBox.deposit{value: value}( + token, + msg.sender, + to, + uint256(amount), + uint256(share) + ); + } + + /// @dev Helper function to withdraw from the `bentoBox`. + function _bentoWithdraw( + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode( + data, + (IERC20, address, int256, int256) + ); + return + bentoBox.withdraw( + token, + msg.sender, + to, + _num(amount, value1, value2), + _num(share, value1, value2) + ); + } + + /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. + /// Calls to `bentoBox` are not allowed for obvious security reasons. + /// This also means that calls made from this contract shall *not* be trusted. + function _call( + uint256 value, + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (bytes memory, uint8) { + ( + address callee, + bytes memory callData, + bool useValue1, + bool useValue2, + uint8 returnValues + ) = abi.decode(data, (address, bytes, bool, bool, uint8)); + + if (useValue1 && !useValue2) { + callData = abi.encodePacked(callData, value1); + } else if (!useValue1 && useValue2) { + callData = abi.encodePacked(callData, value2); + } else if (useValue1 && useValue2) { + callData = abi.encodePacked(callData, value1, value2); + } + + require( + callee != address(bentoBox) && callee != address(this), + "PrivatePool: can't call" + ); + + (bool success, bytes memory returnData) = callee.call{value: value}( + callData + ); + require(success, "PrivatePool: call failed"); + return (returnData, returnValues); + } + + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + for (uint256 i = 0; i < actions.length; i++) { + uint8 action = actions[i]; + if (action == ACTION_ADD_COLLATERAL) { + (uint256 tokenId, address to, bool skim) = abi.decode( + datas[i], + (uint256, address, bool) + ); + addCollateral(tokenId, to, skim); + } else if (action == ACTION_ADD_ASSET) { + (int256 share, bool skim) = abi.decode( + datas[i], + (int256, bool) + ); + addAsset(skim, _num(share, value1, value2)); + } else if (action == ACTION_REPAY) { + (uint256 tokenId, bool skim) = abi.decode( + datas[i], + (uint256, bool) + ); + repay(tokenId, skim); + } else if (action == ACTION_REMOVE_ASSET) { + (int256 share, address to) = abi.decode( + datas[i], + (int256, address) + ); + removeAsset(to, _num(share, value1, value2)); + } else if (action == ACTION_REMOVE_COLLATERAL) { + (uint256 tokenId, address to) = abi.decode( + datas[i], + (uint256, address) + ); + removeCollateral(tokenId, to); + } else if (action == ACTION_BORROW) { + ( + uint256 tokenId, + address to, + uint128 minValuation, + uint64 minExpiration, + uint16 maxOpenFeeBPS, + uint16 maxAnnualInterestBPS + ) = abi.decode( + datas[i], + ( + uint256, + address, + uint128, + uint64, + uint16, + uint16 + ) + ); + (value1, value2) = borrow( + tokenId, + to, + minValuation, + minExpiration, + maxOpenFeeBPS, + maxAnnualInterestBPS + ); + } else if (action == ACTION_BENTO_SETAPPROVAL) { + ( + address user, + address _masterContract, + bool approved, + uint8 v, + bytes32 r, + bytes32 s + ) = abi.decode( + datas[i], + (address, address, bool, uint8, bytes32, bytes32) + ); + bentoBox.setMasterContractApproval( + user, + _masterContract, + approved, + v, + r, + s + ); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit( + datas[i], + values[i], + value1, + value2 + ); + } else if (action == ACTION_BENTO_WITHDRAW) { + (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + } else if (action == ACTION_BENTO_TRANSFER) { + (IERC20 token, address to, int256 share) = abi.decode( + datas[i], + (IERC20, address, int256) + ); + bentoBox.transfer( + token, + msg.sender, + to, + _num(share, value1, value2) + ); + } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { + ( + IERC20 token, + address[] memory tos, + uint256[] memory shares + ) = abi.decode(datas[i], (IERC20, address[], uint256[])); + bentoBox.transferMultiple(token, msg.sender, tos, shares); + } else if (action == ACTION_CALL) { + (bytes memory returnData, uint8 returnValues) = _call( + values[i], + datas[i], + value1, + value2 + ); + + if (returnValues == 1) { + (value1) = abi.decode(returnData, (uint256)); + } else if (returnValues == 2) { + (value1, value2) = abi.decode( + returnData, + (uint256, uint256) + ); + } + } + } + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + address to = masterContract.feeTo(); + + uint256 assetShare = assetBalance.feesEarnedShare; + if (assetShare > 0) { + bentoBox.transfer(asset, address(this), to, assetShare); + assetBalance.feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, assetShare); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } +} diff --git a/package.json b/package.json index c3e32206..d9c9f887 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@nomiclabs/hardhat-etherscan": "^2.1.6", "@nomiclabs/hardhat-solhint": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.1", + "@openzeppelin/contracts": "^3", "@sushiswap/bentobox-sdk": "sushiswap/bentobox-sdk#e3f809870b86afbd57e0930662221d2362a160db", "@sushiswap/core": "^1.4.2", "@tenderly/hardhat-tenderly": "^1.0.12", diff --git a/yarn.lock b/yarn.lock index 7112e675..13740683 100644 --- a/yarn.lock +++ b/yarn.lock @@ -792,6 +792,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== +"@openzeppelin/contracts@^3": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" + integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== + "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" From 2f2e66cd1b843ca54131822e314db2f0c720616b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 26 Dec 2021 03:35:18 +1100 Subject: [PATCH 018/107] Extract old OpenZeppelin code to avoid conflicts --- contracts/PrivatePoolNFT.sol | 4 +- contracts/interfaces/IERC165.sol | 25 +++++ contracts/interfaces/IERC721.sol | 130 +++++++++++++++++++++++ contracts/interfaces/IERC721Receiver.sol | 22 ++++ package.json | 1 - yarn.lock | 5 - 6 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 contracts/interfaces/IERC165.sol create mode 100644 contracts/interfaces/IERC721.sol create mode 100644 contracts/interfaces/IERC721Receiver.sol diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index bffb393c..338ec192 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -25,8 +25,8 @@ import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "./interfaces/IERC721.sol"; +import "./interfaces/IERC721Receiver.sol"; /// @title PrivatePoolNFT /// @dev This contract allows contract calls to any contract (except BentoBox) diff --git a/contracts/interfaces/IERC165.sol b/contracts/interfaces/IERC165.sol new file mode 100644 index 00000000..4ba01172 --- /dev/null +++ b/contracts/interfaces/IERC165.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// Taken from OpenZeppelin contracts v3 + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/interfaces/IERC721.sol b/contracts/interfaces/IERC721.sol new file mode 100644 index 00000000..5a1465ce --- /dev/null +++ b/contracts/interfaces/IERC721.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +// Taken from OpenZeppelin contracts v3 + +pragma solidity >=0.6.2 <0.8.0; + +import "./IERC165.sol"; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 is IERC165 { + /** + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + */ + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. + */ + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. + */ + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the number of tokens in ``owner``'s account. + */ + function balanceOf(address owner) external view returns (uint256 balance); + + /** + * @dev Returns the owner of the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Transfers `tokenId` token from `from` to `to`. + * + * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to transfer `tokenId` token to another account. + * The approval is cleared when the token is transferred. + * + * Only a single account can be approved at a time, so approving the zero address clears previous approvals. + * + * Requirements: + * + * - The caller must own the token or be an approved operator. + * - `tokenId` must exist. + * + * Emits an {Approval} event. + */ + function approve(address to, uint256 tokenId) external; + + /** + * @dev Returns the account approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Approve or remove `operator` as an operator for the caller. + * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {ApprovalForAll} event. + */ + function setApprovalForAll(address operator, bool _approved) external; + + /** + * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. + * + * See {setApprovalForAll} + */ + function isApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} diff --git a/contracts/interfaces/IERC721Receiver.sol b/contracts/interfaces/IERC721Receiver.sol new file mode 100644 index 00000000..c64a0521 --- /dev/null +++ b/contracts/interfaces/IERC721Receiver.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +// Taken from OpenZeppelin contracts v3 + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @title ERC721 token receiver interface + * @dev Interface for any contract that wants to support safeTransfers + * from ERC721 asset contracts. + */ +interface IERC721Receiver { + /** + * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} + * by `operator` from `from`, this function is called. + * + * It must return its Solidity selector to confirm the token transfer. + * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + * + * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. + */ + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} diff --git a/package.json b/package.json index d9c9f887..c3e32206 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@nomiclabs/hardhat-etherscan": "^2.1.6", "@nomiclabs/hardhat-solhint": "^2.0.0", "@nomiclabs/hardhat-waffle": "^2.0.1", - "@openzeppelin/contracts": "^3", "@sushiswap/bentobox-sdk": "sushiswap/bentobox-sdk#e3f809870b86afbd57e0930662221d2362a160db", "@sushiswap/core": "^1.4.2", "@tenderly/hardhat-tenderly": "^1.0.12", diff --git a/yarn.lock b/yarn.lock index 13740683..7112e675 100644 --- a/yarn.lock +++ b/yarn.lock @@ -792,11 +792,6 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== -"@openzeppelin/contracts@^3": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2.tgz#d81f786fda2871d1eb8a8c5a73e455753ba53527" - integrity sha512-z0zMCjyhhp4y7XKAcDAi3Vgms4T2PstwBdahiO0+9NaGICQKjynK3wduSRplTgk4LXmoO1yfDGO5RbjKYxtuxA== - "@resolver-engine/core@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@resolver-engine/core/-/core-0.3.3.tgz#590f77d85d45bc7ecc4e06c654f41345db6ca967" From c712693d36f650bf4318cf867975367388407de9 Mon Sep 17 00:00:00 2001 From: 0xCalibur <0xCalibur@protonmail.com> Date: Mon, 24 Jan 2022 15:45:30 -0500 Subject: [PATCH 019/107] rollback to original formatting setting --- .prettierrc.js | 4 +- archive/contracts/KashiPair.sol | 707 ++++++++++++++++++ .../deploy}/BobaMoonriverDegenBox.ts | 0 .../BobaMoonriverNetworkForkTest.test.ts | 0 contracts/PrivatePool.sol | 405 ++-------- contracts/PrivatePoolNFT.sol | 243 ++---- test/PrivatePool.forking.test.ts | 8 +- 7 files changed, 838 insertions(+), 529 deletions(-) create mode 100644 archive/contracts/KashiPair.sol rename {deploy => archive/deploy}/BobaMoonriverDegenBox.ts (100%) rename {test => archive/test}/BobaMoonriverNetworkForkTest.test.ts (100%) diff --git a/.prettierrc.js b/.prettierrc.js index e8e309d0..34e65a52 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -3,7 +3,7 @@ module.exports = { { files: "*.ts", options: { - printWidth: 80, + printWidth: 145, semi: true, trailingComma: "es5", }, @@ -11,7 +11,7 @@ module.exports = { { files: "*.sol", options: { - printWidth: 80, + printWidth: 140, tabWidth: 4, useTabs: false, singleQuote: false, diff --git a/archive/contracts/KashiPair.sol b/archive/contracts/KashiPair.sol new file mode 100644 index 00000000..b3c548eb --- /dev/null +++ b/archive/contracts/KashiPair.sol @@ -0,0 +1,707 @@ +// SPDX-License-Identifier: UNLICENSED +// Kashi Lending Medium Risk + +// __ __ __ __ _____ __ __ +// | |/ .---.-.-----| |--|__| | |_.-----.-----.--| |__.-----.-----. +// | <| _ |__ --| | | | | -__| | _ | | | _ | +// |__|\__|___._|_____|__|__|__| |_______|_____|__|__|_____|__|__|__|___ | +// |_____| + +// Copyright (c) 2021 BoringCrypto - All rights reserved +// Twitter: @Boring_Crypto + +// Special thanks to: +// @0xKeno - for all his invaluable contributions +// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations + +// Version: 22-Feb-2021 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +// solhint-disable avoid-low-level-calls +// solhint-disable no-inline-assembly +// solhint-disable not-rely-on-time + +// File contracts/KashiPair.sol +// License-Identifier: UNLICENSED +// Kashi Lending Medium Risk + +/// @title KashiPair +/// @dev This contract allows contract calls to any contract (except BentoBox) +/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. +contract KashiPairMediumRiskV1 is ERC20, BoringOwnable, IMasterContract { + using BoringMath for uint256; + using BoringMath128 for uint128; + using RebaseLibrary for Rebase; + using BoringERC20 for IERC20; + + event LogExchangeRate(uint256 rate); + event LogAccrue(uint256 accruedAmount, uint256 feeFraction, uint64 rate, uint256 utilization); + event LogAddCollateral(address indexed from, address indexed to, uint256 share); + event LogAddAsset(address indexed from, address indexed to, uint256 share, uint256 fraction); + event LogRemoveCollateral(address indexed from, address indexed to, uint256 share); + event LogRemoveAsset(address indexed from, address indexed to, uint256 share, uint256 fraction); + event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 feeAmount, uint256 part); + event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction); + + // Immutables (for MasterContract and all clones) + IBentoBoxV1 public immutable bentoBox; + KashiPairMediumRiskV1 public immutable masterContract; + + // MasterContract variables + address public feeTo; + mapping(ISwapper => bool) public swappers; + + // Per clone variables + // Clone init settings + IERC20 public collateral; + IERC20 public asset; + IOracle public oracle; + bytes public oracleData; + + // Total amounts + uint256 public totalCollateralShare; // Total collateral supplied + Rebase public totalAsset; // elastic = BentoBox shares held by the KashiPair, base = Total fractions held by asset suppliers + Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers + + // User balances + mapping(address => uint256) public userCollateralShare; + // userAssetFraction is called balanceOf for ERC20 compatibility (it's in ERC20.sol) + mapping(address => uint256) public userBorrowPart; + + /// @notice Exchange and interest rate tracking. + /// This is 'cached' here because calls to Oracles can be very expensive. + uint256 public exchangeRate; + + struct AccrueInfo { + uint64 interestPerSecond; + uint64 lastAccrued; + uint128 feesEarnedFraction; + } + + AccrueInfo public accrueInfo; + + // ERC20 'variables' + function symbol() external view returns (string memory) { + return string(abi.encodePacked("km", collateral.safeSymbol(), "/", asset.safeSymbol(), "-", oracle.symbol(oracleData))); + } + + function name() external view returns (string memory) { + return string(abi.encodePacked("Kashi Medium Risk ", collateral.safeName(), "/", asset.safeName(), "-", oracle.name(oracleData))); + } + + function decimals() external view returns (uint8) { + return asset.safeDecimals(); + } + + // totalSupply for ERC20 compatibility + function totalSupply() public view returns (uint256) { + return totalAsset.base; + } + + // Settings for the Medium Risk KashiPair + uint256 private constant CLOSED_COLLATERIZATION_RATE = 75000; // 75% + uint256 private constant OPEN_COLLATERIZATION_RATE = 77000; // 77% + uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math) + uint256 private constant MINIMUM_TARGET_UTILIZATION = 7e17; // 70% + uint256 private constant MAXIMUM_TARGET_UTILIZATION = 8e17; // 80% + uint256 private constant UTILIZATION_PRECISION = 1e18; + uint256 private constant FULL_UTILIZATION = 1e18; + uint256 private constant FULL_UTILIZATION_MINUS_MAX = FULL_UTILIZATION - MAXIMUM_TARGET_UTILIZATION; + uint256 private constant FACTOR_PRECISION = 1e18; + + uint64 private constant STARTING_INTEREST_PER_SECOND = 317097920; // approx 1% APR + uint64 private constant MINIMUM_INTEREST_PER_SECOND = 79274480; // approx 0.25% APR + uint64 private constant MAXIMUM_INTEREST_PER_SECOND = 317097920000; // approx 1000% APR + uint256 private constant INTEREST_ELASTICITY = 28800e36; // Half or double in 28800 seconds (8 hours) if linear + + uint256 private constant EXCHANGE_RATE_PRECISION = 1e18; + + uint256 private constant LIQUIDATION_MULTIPLIER = 112000; // add 12% + uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5; + + // Fees + uint256 private constant PROTOCOL_FEE = 10000; // 10% + uint256 private constant PROTOCOL_FEE_DIVISOR = 1e5; + uint256 private constant BORROW_OPENING_FEE = 50; // 0.05% + uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5; + + /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`. + constructor(IBentoBoxV1 bentoBox_) public { + bentoBox = bentoBox_; + masterContract = this; + feeTo = msg.sender; + } + + /// @notice Serves as the constructor for clones, as clones can't have a regular constructor + /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData) + function init(bytes calldata data) public payable override { + require(address(collateral) == address(0), "KashiPair: already initialized"); + (collateral, asset, oracle, oracleData) = abi.decode(data, (IERC20, IERC20, IOracle, bytes)); + require(address(collateral) != address(0), "KashiPair: bad pair"); + + accrueInfo.interestPerSecond = uint64(STARTING_INTEREST_PER_SECOND); // 1% APR, with 1e18 being 100% + } + + /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees. + function accrue() public { + AccrueInfo memory _accrueInfo = accrueInfo; + // Number of seconds since accrue was called + uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued; + if (elapsedTime == 0) { + return; + } + _accrueInfo.lastAccrued = uint64(block.timestamp); + + Rebase memory _totalBorrow = totalBorrow; + if (_totalBorrow.base == 0) { + // If there are no borrows, reset the interest rate + if (_accrueInfo.interestPerSecond != STARTING_INTEREST_PER_SECOND) { + _accrueInfo.interestPerSecond = STARTING_INTEREST_PER_SECOND; + emit LogAccrue(0, 0, STARTING_INTEREST_PER_SECOND, 0); + } + accrueInfo = _accrueInfo; + return; + } + + uint256 extraAmount = 0; + uint256 feeFraction = 0; + Rebase memory _totalAsset = totalAsset; + + // Accrue interest + extraAmount = uint256(_totalBorrow.elastic).mul(_accrueInfo.interestPerSecond).mul(elapsedTime) / 1e18; + _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount.to128()); + uint256 fullAssetAmount = bentoBox.toAmount(asset, _totalAsset.elastic, false).add(_totalBorrow.elastic); + + uint256 feeAmount = extraAmount.mul(PROTOCOL_FEE) / PROTOCOL_FEE_DIVISOR; // % of interest paid goes to fee + feeFraction = feeAmount.mul(_totalAsset.base) / fullAssetAmount; + _accrueInfo.feesEarnedFraction = _accrueInfo.feesEarnedFraction.add(feeFraction.to128()); + totalAsset.base = _totalAsset.base.add(feeFraction.to128()); + totalBorrow = _totalBorrow; + + // Update interest rate + uint256 utilization = uint256(_totalBorrow.elastic).mul(UTILIZATION_PRECISION) / fullAssetAmount; + if (utilization < MINIMUM_TARGET_UTILIZATION) { + uint256 underFactor = MINIMUM_TARGET_UTILIZATION.sub(utilization).mul(FACTOR_PRECISION) / MINIMUM_TARGET_UTILIZATION; + uint256 scale = INTEREST_ELASTICITY.add(underFactor.mul(underFactor).mul(elapsedTime)); + _accrueInfo.interestPerSecond = uint64(uint256(_accrueInfo.interestPerSecond).mul(INTEREST_ELASTICITY) / scale); + + if (_accrueInfo.interestPerSecond < MINIMUM_INTEREST_PER_SECOND) { + _accrueInfo.interestPerSecond = MINIMUM_INTEREST_PER_SECOND; // 0.25% APR minimum + } + } else if (utilization > MAXIMUM_TARGET_UTILIZATION) { + uint256 overFactor = utilization.sub(MAXIMUM_TARGET_UTILIZATION).mul(FACTOR_PRECISION) / FULL_UTILIZATION_MINUS_MAX; + uint256 scale = INTEREST_ELASTICITY.add(overFactor.mul(overFactor).mul(elapsedTime)); + uint256 newInterestPerSecond = uint256(_accrueInfo.interestPerSecond).mul(scale) / INTEREST_ELASTICITY; + if (newInterestPerSecond > MAXIMUM_INTEREST_PER_SECOND) { + newInterestPerSecond = MAXIMUM_INTEREST_PER_SECOND; // 1000% APR maximum + } + _accrueInfo.interestPerSecond = uint64(newInterestPerSecond); + } + + emit LogAccrue(extraAmount, feeFraction, _accrueInfo.interestPerSecond, utilization); + accrueInfo = _accrueInfo; + } + + /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`. + /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls. + function _isSolvent( + address user, + bool open, + uint256 _exchangeRate + ) internal view returns (bool) { + // accrue must have already been called! + uint256 borrowPart = userBorrowPart[user]; + if (borrowPart == 0) return true; + uint256 collateralShare = userCollateralShare[user]; + if (collateralShare == 0) return false; + + Rebase memory _totalBorrow = totalBorrow; + + return + bentoBox.toAmount( + collateral, + collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul( + open ? OPEN_COLLATERIZATION_RATE : CLOSED_COLLATERIZATION_RATE + ), + false + ) >= + // Moved exchangeRate here instead of dividing the other side to preserve more precision + borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base; + } + + /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body. + modifier solvent() { + _; + require(_isSolvent(msg.sender, false, exchangeRate), "KashiPair: user insolvent"); + } + + /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset. + /// This function is supposed to be invoked if needed because Oracle queries can be expensive. + /// @return updated True if `exchangeRate` was updated. + /// @return rate The new exchange rate. + function updateExchangeRate() public returns (bool updated, uint256 rate) { + (updated, rate) = oracle.get(oracleData); + + if (updated) { + exchangeRate = rate; + emit LogExchangeRate(rate); + } else { + // Return the old rate if fetching wasn't successful + rate = exchangeRate; + } + } + + /// @dev Helper function to move tokens. + /// @param token The ERC-20 token. + /// @param share The amount in shares to add. + /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True. + /// Only used for accounting checks. + /// @param skim If True, only does a balance check on this contract. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + function _addTokens( + IERC20 token, + uint256 share, + uint256 total, + bool skim + ) internal { + if (skim) { + require(share <= bentoBox.balanceOf(token, address(this)).sub(total), "KashiPair: Skim too much"); + } else { + bentoBox.transfer(token, msg.sender, address(this), share); + } + } + + /// @notice Adds `collateral` from msg.sender to the account `to`. + /// @param to The receiver of the tokens. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param share The amount of shares to add for `to`. + function addCollateral( + address to, + bool skim, + uint256 share + ) public { + userCollateralShare[to] = userCollateralShare[to].add(share); + uint256 oldTotalCollateralShare = totalCollateralShare; + totalCollateralShare = oldTotalCollateralShare.add(share); + _addTokens(collateral, share, oldTotalCollateralShare, skim); + emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share); + } + + /// @dev Concrete implementation of `removeCollateral`. + function _removeCollateral(address to, uint256 share) internal { + userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share); + totalCollateralShare = totalCollateralShare.sub(share); + emit LogRemoveCollateral(msg.sender, to, share); + bentoBox.transfer(collateral, address(this), to, share); + } + + /// @notice Removes `share` amount of collateral and transfers it to `to`. + /// @param to The receiver of the shares. + /// @param share Amount of shares to remove. + function removeCollateral(address to, uint256 share) public solvent { + // accrue must be called because we check solvency + accrue(); + _removeCollateral(to, share); + } + + /// @dev Concrete implementation of `addAsset`. + function _addAsset( + address to, + bool skim, + uint256 share + ) internal returns (uint256 fraction) { + Rebase memory _totalAsset = totalAsset; + uint256 totalAssetShare = _totalAsset.elastic; + uint256 allShare = _totalAsset.elastic + bentoBox.toShare(asset, totalBorrow.elastic, true); + fraction = allShare == 0 ? share : share.mul(_totalAsset.base) / allShare; + if (_totalAsset.base.add(fraction.to128()) < 1000) { + return 0; + } + totalAsset = _totalAsset.add(share, fraction); + balanceOf[to] = balanceOf[to].add(fraction); + _addTokens(asset, share, totalAssetShare, skim); + emit LogAddAsset(skim ? address(bentoBox) : msg.sender, to, share, fraction); + } + + /// @notice Adds assets to the lending pair. + /// @param to The address of the user to receive the assets. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param share The amount of shares to add. + /// @return fraction Total fractions added. + function addAsset( + address to, + bool skim, + uint256 share + ) public returns (uint256 fraction) { + accrue(); + fraction = _addAsset(to, skim, share); + } + + /// @dev Concrete implementation of `removeAsset`. + function _removeAsset(address to, uint256 fraction) internal returns (uint256 share) { + Rebase memory _totalAsset = totalAsset; + uint256 allShare = _totalAsset.elastic + bentoBox.toShare(asset, totalBorrow.elastic, true); + share = fraction.mul(allShare) / _totalAsset.base; + balanceOf[msg.sender] = balanceOf[msg.sender].sub(fraction); + _totalAsset.elastic = _totalAsset.elastic.sub(share.to128()); + _totalAsset.base = _totalAsset.base.sub(fraction.to128()); + require(_totalAsset.base >= 1000, "Kashi: below minimum"); + totalAsset = _totalAsset; + emit LogRemoveAsset(msg.sender, to, share, fraction); + bentoBox.transfer(asset, address(this), to, share); + } + + /// @notice Removes an asset from msg.sender and transfers it to `to`. + /// @param to The user that receives the removed assets. + /// @param fraction The amount/fraction of assets held to remove. + /// @return share The amount of shares transferred to `to`. + function removeAsset(address to, uint256 fraction) public returns (uint256 share) { + accrue(); + share = _removeAsset(to, fraction); + } + + /// @dev Concrete implementation of `borrow`. + function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) { + uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow + + (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true); + userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part); + emit LogBorrow(msg.sender, to, amount, feeAmount, part); + + share = bentoBox.toShare(asset, amount, false); + Rebase memory _totalAsset = totalAsset; + require(_totalAsset.base >= 1000, "Kashi: below minimum"); + _totalAsset.elastic = _totalAsset.elastic.sub(share.to128()); + totalAsset = _totalAsset; + bentoBox.transfer(asset, address(this), to, share); + } + + /// @notice Sender borrows `amount` and transfers it to `to`. + /// @return part Total part of the debt held by borrowers. + /// @return share Total amount in shares borrowed. + function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) { + accrue(); + (part, share) = _borrow(to, amount); + } + + /// @dev Concrete implementation of `repay`. + function _repay( + address to, + bool skim, + uint256 part + ) internal returns (uint256 amount) { + (totalBorrow, amount) = totalBorrow.sub(part, true); + userBorrowPart[to] = userBorrowPart[to].sub(part); + + uint256 share = bentoBox.toShare(asset, amount, true); + uint128 totalShare = totalAsset.elastic; + _addTokens(asset, share, uint256(totalShare), skim); + totalAsset.elastic = totalShare.add(share.to128()); + emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part); + } + + /// @notice Repays a loan. + /// @param to Address of the user this payment should go. + /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. + /// False if tokens from msg.sender in `bentoBox` should be transferred. + /// @param part The amount to repay. See `userBorrowPart`. + /// @return amount The total amount repayed. + function repay( + address to, + bool skim, + uint256 part + ) public returns (uint256 amount) { + accrue(); + amount = _repay(to, skim, part); + } + + // Functions that need accrue to be called + uint8 internal constant ACTION_ADD_ASSET = 1; + uint8 internal constant ACTION_REPAY = 2; + uint8 internal constant ACTION_REMOVE_ASSET = 3; + uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; + uint8 internal constant ACTION_BORROW = 5; + uint8 internal constant ACTION_GET_REPAY_SHARE = 6; + uint8 internal constant ACTION_GET_REPAY_PART = 7; + uint8 internal constant ACTION_ACCRUE = 8; + + // Functions that don't need accrue to be called + uint8 internal constant ACTION_ADD_COLLATERAL = 10; + uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11; + + // Function on BentoBox + uint8 internal constant ACTION_BENTO_DEPOSIT = 20; + uint8 internal constant ACTION_BENTO_WITHDRAW = 21; + uint8 internal constant ACTION_BENTO_TRANSFER = 22; + uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; + uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; + + // Any external call (except to BentoBox) + uint8 internal constant ACTION_CALL = 30; + + int256 internal constant USE_VALUE1 = -1; + int256 internal constant USE_VALUE2 = -2; + + /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. + function _num( + int256 inNum, + uint256 value1, + uint256 value2 + ) internal pure returns (uint256 outNum) { + outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); + } + + /// @dev Helper function for depositing into `bentoBox`. + function _bentoDeposit( + bytes memory data, + uint256 value, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors + share = int256(_num(share, value1, value2)); + return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); + } + + /// @dev Helper function to withdraw from the `bentoBox`. + function _bentoWithdraw( + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); + } + + /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. + /// Calls to `bentoBox` are not allowed for obvious security reasons. + /// This also means that calls made from this contract shall *not* be trusted. + function _call( + uint256 value, + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (bytes memory, uint8) { + (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( + data, + (address, bytes, bool, bool, uint8) + ); + + if (useValue1 && !useValue2) { + callData = abi.encodePacked(callData, value1); + } else if (!useValue1 && useValue2) { + callData = abi.encodePacked(callData, value2); + } else if (useValue1 && useValue2) { + callData = abi.encodePacked(callData, value1, value2); + } + + require(callee != address(bentoBox) && callee != address(this), "KashiPair: can't call"); + + (bool success, bytes memory returnData) = callee.call{value: value}(callData); + require(success, "KashiPair: call failed"); + return (returnData, returnValues); + } + + struct CookStatus { + bool needsSolvencyCheck; + bool hasAccrued; + } + + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + CookStatus memory status; + for (uint256 i = 0; i < actions.length; i++) { + uint8 action = actions[i]; + if (!status.hasAccrued && action < 10) { + accrue(); + status.hasAccrued = true; + } + if (action == ACTION_ADD_COLLATERAL) { + (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool)); + addCollateral(to, skim, _num(share, value1, value2)); + } else if (action == ACTION_ADD_ASSET) { + (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool)); + value1 = _addAsset(to, skim, _num(share, value1, value2)); + } else if (action == ACTION_REPAY) { + (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool)); + _repay(to, skim, _num(part, value1, value2)); + } else if (action == ACTION_REMOVE_ASSET) { + (int256 fraction, address to) = abi.decode(datas[i], (int256, address)); + value1 = _removeAsset(to, _num(fraction, value1, value2)); + } else if (action == ACTION_REMOVE_COLLATERAL) { + (int256 share, address to) = abi.decode(datas[i], (int256, address)); + _removeCollateral(to, _num(share, value1, value2)); + status.needsSolvencyCheck = true; + } else if (action == ACTION_BORROW) { + (int256 amount, address to) = abi.decode(datas[i], (int256, address)); + (value1, value2) = _borrow(to, _num(amount, value1, value2)); + status.needsSolvencyCheck = true; + } else if (action == ACTION_UPDATE_EXCHANGE_RATE) { + (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256)); + (bool updated, uint256 rate) = updateExchangeRate(); + require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), "KashiPair: rate not ok"); + } else if (action == ACTION_BENTO_SETAPPROVAL) { + (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( + datas[i], + (address, address, bool, uint8, bytes32, bytes32) + ); + bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); + } else if (action == ACTION_BENTO_WITHDRAW) { + (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + } else if (action == ACTION_BENTO_TRANSFER) { + (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); + bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); + } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { + (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); + bentoBox.transferMultiple(token, msg.sender, tos, shares); + } else if (action == ACTION_CALL) { + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); + + if (returnValues == 1) { + (value1) = abi.decode(returnData, (uint256)); + } else if (returnValues == 2) { + (value1, value2) = abi.decode(returnData, (uint256, uint256)); + } + } else if (action == ACTION_GET_REPAY_SHARE) { + int256 part = abi.decode(datas[i], (int256)); + value1 = bentoBox.toShare(asset, totalBorrow.toElastic(_num(part, value1, value2), true), true); + } else if (action == ACTION_GET_REPAY_PART) { + int256 amount = abi.decode(datas[i], (int256)); + value1 = totalBorrow.toBase(_num(amount, value1, value2), false); + } + } + + if (status.needsSolvencyCheck) { + require(_isSolvent(msg.sender, false, exchangeRate), "KashiPair: user insolvent"); + } + } + + /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low. + /// @param users An array of user addresses. + /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user. + /// @param to Address of the receiver in open liquidations if `swapper` is zero. + /// @param swapper Contract address of the `ISwapper` implementation. Swappers are restricted for closed liquidations. See `setSwapper`. + /// @param open True to perform a open liquidation else False. + function liquidate( + address[] calldata users, + uint256[] calldata maxBorrowParts, + address to, + ISwapper swapper, + bool open + ) public { + // Oracle can fail but we still need to allow liquidations + (, uint256 _exchangeRate) = updateExchangeRate(); + accrue(); + + uint256 allCollateralShare; + uint256 allBorrowAmount; + uint256 allBorrowPart; + Rebase memory _totalBorrow = totalBorrow; + Rebase memory bentoBoxTotals = bentoBox.totals(collateral); + for (uint256 i = 0; i < users.length; i++) { + address user = users[i]; + if (!_isSolvent(user, open, _exchangeRate)) { + uint256 borrowPart; + { + uint256 availableBorrowPart = userBorrowPart[user]; + borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i]; + userBorrowPart[user] = availableBorrowPart.sub(borrowPart); + } + uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false); + uint256 collateralShare = bentoBoxTotals.toBase( + borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) / + (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION), + false + ); + + userCollateralShare[user] = userCollateralShare[user].sub(collateralShare); + emit LogRemoveCollateral(user, swapper == ISwapper(0) ? to : address(swapper), collateralShare); + emit LogRepay(swapper == ISwapper(0) ? msg.sender : address(swapper), user, borrowAmount, borrowPart); + + // Keep totals + allCollateralShare = allCollateralShare.add(collateralShare); + allBorrowAmount = allBorrowAmount.add(borrowAmount); + allBorrowPart = allBorrowPart.add(borrowPart); + } + } + require(allBorrowAmount != 0, "KashiPair: all are solvent"); + _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128()); + _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128()); + totalBorrow = _totalBorrow; + totalCollateralShare = totalCollateralShare.sub(allCollateralShare); + + uint256 allBorrowShare = bentoBox.toShare(asset, allBorrowAmount, true); + + if (!open) { + // Closed liquidation using a pre-approved swapper for the benefit of the LPs + require(masterContract.swappers(swapper), "KashiPair: Invalid swapper"); + + // Swaps the users' collateral for the borrowed asset + bentoBox.transfer(collateral, address(this), address(swapper), allCollateralShare); + swapper.swap(collateral, asset, address(this), allBorrowShare, allCollateralShare); + + uint256 returnedShare = bentoBox.balanceOf(asset, address(this)).sub(uint256(totalAsset.elastic)); + uint256 extraShare = returnedShare.sub(allBorrowShare); + uint256 feeShare = extraShare.mul(PROTOCOL_FEE) / PROTOCOL_FEE_DIVISOR; // % of profit goes to fee + // solhint-disable-next-line reentrancy + bentoBox.transfer(asset, address(this), masterContract.feeTo(), feeShare); + totalAsset.elastic = totalAsset.elastic.add(returnedShare.sub(feeShare).to128()); + emit LogAddAsset(address(swapper), address(this), extraShare.sub(feeShare), 0); + } else { + // Swap using a swapper freely chosen by the caller + // Open (flash) liquidation: get proceeds first and provide the borrow after + bentoBox.transfer(collateral, address(this), swapper == ISwapper(0) ? to : address(swapper), allCollateralShare); + if (swapper != ISwapper(0)) { + swapper.swap(collateral, asset, msg.sender, allBorrowShare, allCollateralShare); + } + + bentoBox.transfer(asset, msg.sender, address(this), allBorrowShare); + totalAsset.elastic = totalAsset.elastic.add(allBorrowShare.to128()); + } + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + accrue(); + address _feeTo = masterContract.feeTo(); + uint256 _feesEarnedFraction = accrueInfo.feesEarnedFraction; + balanceOf[_feeTo] = balanceOf[_feeTo].add(_feesEarnedFraction); + accrueInfo.feesEarnedFraction = 0; + + emit LogWithdrawFees(_feeTo, _feesEarnedFraction); + } + + /// @notice Used to register and enable or disable swapper contracts used in closed liquidations. + /// MasterContract Only Admin function. + /// @param swapper The address of the swapper contract that conforms to `ISwapper`. + /// @param enable True to enable the swapper. To disable use False. + function setSwapper(ISwapper swapper, bool enable) public onlyOwner { + swappers[swapper] = enable; + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } +} diff --git a/deploy/BobaMoonriverDegenBox.ts b/archive/deploy/BobaMoonriverDegenBox.ts similarity index 100% rename from deploy/BobaMoonriverDegenBox.ts rename to archive/deploy/BobaMoonriverDegenBox.ts diff --git a/test/BobaMoonriverNetworkForkTest.test.ts b/archive/test/BobaMoonriverNetworkForkTest.test.ts similarity index 100% rename from test/BobaMoonriverNetworkForkTest.test.ts rename to archive/test/BobaMoonriverNetworkForkTest.test.ts diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index a134b596..346144ca 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -20,6 +20,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; + import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; @@ -40,43 +41,15 @@ contract PrivatePool is BoringOwnable, IMasterContract { event LogExchangeRate(uint256 rate); event LogAccrue(uint256 accruedAmount, uint256 feeAmount); - event LogAddCollateral( - address indexed from, - address indexed to, - uint256 share - ); + event LogAddCollateral(address indexed from, address indexed to, uint256 share); event LogAddAsset(address indexed from, uint256 share); - event LogRemoveCollateral( - address indexed from, - address indexed to, - uint256 share - ); + event LogRemoveCollateral(address indexed from, address indexed to, uint256 share); event LogRemoveAsset(address indexed to, uint256 share); - event LogBorrow( - address indexed from, - address indexed to, - uint256 amount, - uint256 openFeeAmount, - uint256 part - ); - event LogRepay( - address indexed from, - address indexed to, - uint256 amount, - uint256 part - ); - event LogSeizeCollateral( - address indexed from, - uint256 collateralShare, - uint256 debtAmount, - uint256 debtPart - ); + event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 openFeeAmount, uint256 part); + event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part); + event LogSeizeCollateral(address indexed from, uint256 collateralShare, uint256 debtAmount, uint256 debtPart); event LogFeeTo(address indexed newFeeTo); - event LogWithdrawFees( - address indexed feeTo, - uint256 assetFeeShare, - uint256 collateralFeeShare - ); + event LogWithdrawFees(address indexed feeTo, uint256 assetFeeShare, uint256 collateralFeeShare); // Immutables (for MasterContract and all clones) IBentoBoxV1 public immutable bentoBox; @@ -165,24 +138,12 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @notice Serves as the constructor for clones, as clones can't have a regular constructor function init(bytes calldata data) public payable override { - require( - address(collateral) == address(0), - "PrivatePool: already initialized" - ); + require(address(collateral) == address(0), "PrivatePool: already initialized"); InitSettings memory settings = abi.decode(data, (InitSettings)); - require( - address(settings.collateral) != address(0), - "PrivatePool: bad pair" - ); - require( - settings.LIQUIDATION_MULTIPLIER_BPS >= BPS, - "PrivatePool: negative liquidation bonus" - ); - require( - settings.COLLATERALIZATION_RATE_BPS <= BPS, - "PrivatePool: bad collateralization rate" - ); + require(address(settings.collateral) != address(0), "PrivatePool: bad pair"); + require(settings.LIQUIDATION_MULTIPLIER_BPS >= BPS, "PrivatePool: negative liquidation bonus"); + require(settings.COLLATERALIZATION_RATE_BPS <= BPS, "PrivatePool: bad collateralization rate"); collateral = settings.collateral; asset = settings.asset; @@ -196,8 +157,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS; _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS; _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS; - _aI.LIQUIDATION_SEIZE_COLLATERAL = settings - .LIQUIDATION_SEIZE_COLLATERAL; + _aI.LIQUIDATION_SEIZE_COLLATERAL = settings.LIQUIDATION_SEIZE_COLLATERAL; accrueInfo = _aI; for (uint256 i = 0; i < settings.borrowers.length; i++) { @@ -223,9 +183,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // - _totalDebt.elastic is 128 bits // - INTEREST_PER_SECOND is 64 bits // - elapsedTime fits in 64 bits for the next 580 billion (10^9) years - uint256 extraAmount = (uint256(_totalDebt.elastic) * - _accrueInfo.INTEREST_PER_SECOND * - elapsedTime) / 1e18; + uint256 extraAmount = (uint256(_totalDebt.elastic) * _accrueInfo.INTEREST_PER_SECOND * elapsedTime) / 1e18; // If the interest rate is too high, then this will overflow and // effectively lock up all funds. Do not set the interest rate too @@ -245,11 +203,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 feeShare = bentoBox.toShare(asset, feeAmount, false); if (_assetBalance.reservesShare < feeShare) { _assetBalance.feesEarnedShare += _assetBalance.reservesShare; - feesOwedAmount += bentoBox.toAmount( - asset, - feeShare - _assetBalance.reservesShare, - false - ); + feesOwedAmount += bentoBox.toAmount(asset, feeShare - _assetBalance.reservesShare, false); _assetBalance.reservesShare = 0; } else { // feesEarned + fee <= feesEarned + reserves <= Bento balance: @@ -264,11 +218,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`. /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls. - function _isSolvent(address borrower, uint256 _exchangeRate) - internal - view - returns (bool) - { + function _isSolvent(address borrower, uint256 _exchangeRate) internal view returns (bool) { // accrue must have already been called! uint256 debtPart = borrowerDebtPart[borrower]; if (debtPart == 0) return true; @@ -350,17 +300,12 @@ contract PrivatePool is BoringOwnable, IMasterContract { (uint256 leftLo, uint256 leftHi) = FullMath.fullMul( collateralShare * bentoBoxTotals.elastic, // Cast needed to avoid uint128 overflow - uint256(accrueInfo.COLLATERALIZATION_RATE_BPS) * - _totalDebt.base * - 1e14 + uint256(accrueInfo.COLLATERALIZATION_RATE_BPS) * _totalDebt.base * 1e14 ); uint256 rightLo; uint256 rightHi; if (_exchangeRate <= type(uint128).max) { - (rightLo, rightHi) = FullMath.fullMul( - debtPart * _totalDebt.elastic, - _exchangeRate * bentoBoxTotals.base - ); + (rightLo, rightHi) = FullMath.fullMul(debtPart * _totalDebt.elastic, _exchangeRate * bentoBoxTotals.base); } else { // We multiply it out in stages to be safe. If the total overflows // 512 bits then we are done, as the LHS is guaranteed to be less. @@ -372,10 +317,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // bHi * 2^512 + bLo * 2^256 = xr * aHi * 2^256 // cHi * 2^256 + cLo = xr * aLo // - (uint256 aLo, uint256 aHi) = FullMath.fullMul( - debtPart * _totalDebt.elastic, - bentoBoxTotals.base - ); + (uint256 aLo, uint256 aHi) = FullMath.fullMul(debtPart * _totalDebt.elastic, bentoBoxTotals.base); (uint256 bLo, uint256 bHi) = FullMath.fullMul(_exchangeRate, aHi); if (bHi > 0) { return false; @@ -394,10 +336,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body. modifier solvent() { _; - require( - _isSolvent(msg.sender, exchangeRate), - "PrivatePool: borrower insolvent" - ); + require(_isSolvent(msg.sender, exchangeRate), "PrivatePool: borrower insolvent"); } /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset. @@ -430,10 +369,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { bool skim ) internal { if (skim) { - require( - share <= bentoBox.balanceOf(token, address(this)).sub(total), - "PrivatePool: skim too much" - ); + require(share <= bentoBox.balanceOf(token, address(this)).sub(total), "PrivatePool: skim too much"); } else { bentoBox.transfer(token, msg.sender, address(this), share); } @@ -450,32 +386,24 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 share ) public { uint256 supplied = userCollateralShare[to]; - require( - supplied > 0 || approvedBorrowers[to], - "PrivatePool: unapproved borrower" - ); + require(supplied > 0 || approvedBorrowers[to], "PrivatePool: unapproved borrower"); // No overflow: the total for ALL users fits in 128 bits, or the // BentoBox transfer (_addTokens) fails. userCollateralShare[to] = supplied + share; CollateralBalance memory _collateralBalance = collateralBalance; // No overflow: the sum fits in the BentoBox total - uint256 prevTotal = _collateralBalance.userTotalShare + - _collateralBalance.feesEarnedShare; + uint256 prevTotal = _collateralBalance.userTotalShare + _collateralBalance.feesEarnedShare; // No overflow if cast safe: fits in the BentoBox or _addTokens reverts // Cast safe: _addTokens does not truncate the value - collateralBalance.userTotalShare = - _collateralBalance.userTotalShare + - uint128(share); + collateralBalance.userTotalShare = _collateralBalance.userTotalShare + uint128(share); _addTokens(collateral, share, prevTotal, skim); emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share); } /// @dev Concrete implementation of `removeCollateral`. function _removeCollateral(address to, uint256 share) internal { - userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub( - share - ); + userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share); // No underflow: userTotalShare > userCollateralShare[msg.sender] // Cast safe: Bento transfer reverts if it is not. collateralBalance.userTotalShare -= uint128(share); @@ -505,8 +433,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { ) internal { IERC20 _asset = asset; AssetBalance memory _assetBalance = assetBalance; - uint256 priorAssetTotalShare = _assetBalance.reservesShare + - _assetBalance.feesEarnedShare; + uint256 priorAssetTotalShare = _assetBalance.reservesShare + _assetBalance.feesEarnedShare; Rebase memory bentoBoxTotals = bentoBox.totals(_asset); uint256 toFeesShare = 0; @@ -521,27 +448,17 @@ contract PrivatePool is BoringOwnable, IMasterContract { if (_assetBalance.reservesShare == 0) { uint256 _feesOwedAmount = feesOwedAmount; if (_feesOwedAmount > 0) { - uint256 feesOwedShare = bentoBoxTotals.toBase( - _feesOwedAmount, - false - ); + uint256 feesOwedShare = bentoBoxTotals.toBase(_feesOwedAmount, false); // New fees cannot pay off existing fees: if (toReservesShare < feesOwedShare) { - feesOwedAmount = bentoBoxTotals.toElastic( - feesOwedShare - toReservesShare, - false - ); + feesOwedAmount = bentoBoxTotals.toElastic(feesOwedShare - toReservesShare, false); _assetBalance.feesEarnedShare += uint128(takenShare); } else { feesOwedAmount = 0; // No overflow: assuming the transfer at the end succeeds: // feesOwedShare <= toReservesShare <= (Bento balance), - _assetBalance.feesEarnedShare += uint128( - feesOwedShare + toFeesShare - ); - _assetBalance.reservesShare = uint128( - toReservesShare - feesOwedShare - ); + _assetBalance.feesEarnedShare += uint128(feesOwedShare + toFeesShare); + _assetBalance.reservesShare = uint128(toReservesShare - feesOwedShare); } } else { _assetBalance.reservesShare = uint128(toReservesShare); @@ -575,9 +492,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { function _removeAsset(address to, uint256 share) internal { require(msg.sender == lender, "PrivatePool: not the lender"); // Cast safe: Bento transfer reverts unless stronger condition holds - assetBalance.reservesShare = assetBalance.reservesShare.sub( - uint128(share) - ); + assetBalance.reservesShare = assetBalance.reservesShare.sub(uint128(share)); bentoBox.transfer(asset, address(this), to, share); emit LogRemoveAsset(to, share); } @@ -591,14 +506,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { } /// @dev Concrete implementation of `borrow`. - function _borrow(address to, uint256 amount) - internal - returns (uint256 part, uint256 share) - { - require( - approvedBorrowers[msg.sender], - "PrivatePool: unapproved borrower" - ); + function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) { + require(approvedBorrowers[msg.sender], "PrivatePool: unapproved borrower"); IERC20 _asset = asset; Rebase memory bentoBoxTotals = bentoBox.totals(_asset); AccrueInfo memory _accrueInfo = accrueInfo; @@ -609,14 +518,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { // BentoBox shares total if the transfer succeeds. But then "amount" // must fit in the token total; at least up to some rounding error, // which cannot be more than 128 bits either. - uint256 openFeeAmount = (amount * _accrueInfo.BORROW_OPENING_FEE_BPS) / - BPS; + uint256 openFeeAmount = (amount * _accrueInfo.BORROW_OPENING_FEE_BPS) / BPS; // No overflow: Same reason. Also, we just divided by BPS.. uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS; - uint256 protocolFeeShare = bentoBoxTotals.toBase( - protocolFeeAmount, - false - ); + uint256 protocolFeeShare = bentoBoxTotals.toBase(protocolFeeAmount, false); // The protocol component of the opening fee cannot be owed: AssetBalance memory _assetBalance = assetBalance; @@ -625,9 +530,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // these calculations: `share` is not modified. // Theoretically the fee could just make it overflow 128 bits. // Underflow check is core business logic: - _assetBalance.reservesShare = _assetBalance.reservesShare.sub( - (share + protocolFeeShare).to128() - ); + _assetBalance.reservesShare = _assetBalance.reservesShare.sub((share + protocolFeeShare).to128()); // Cast is safe: `share` fits. Also, the checked cast above succeeded. // No overflow: protocolFeeShare < reservesShare, and both balances // together fit in the Bento share balance, @@ -648,11 +551,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @notice Sender borrows `amount` and transfers it to `to`. /// @return part Total part of the debt held by borrowers. /// @return share Total amount in shares borrowed. - function borrow(address to, uint256 amount) - public - solvent - returns (uint256 part, uint256 share) - { + function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) { accrue(); (part, share) = _borrow(to, amount); } @@ -717,9 +616,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 value1, uint256 value2 ) internal pure returns (uint256 outNum) { - outNum = inNum >= 0 - ? uint256(inNum) - : (inNum == USE_VALUE1 ? value1 : value2); + outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); } /// @dev Helper function for depositing into `bentoBox`. @@ -729,20 +626,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 value1, uint256 value2 ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode( - data, - (IERC20, address, int256, int256) - ); + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors share = int256(_num(share, value1, value2)); - return - bentoBox.deposit{value: value}( - token, - msg.sender, - to, - uint256(amount), - uint256(share) - ); + return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); } /// @dev Helper function to withdraw from the `bentoBox`. @@ -751,18 +638,8 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 value1, uint256 value2 ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode( - data, - (IERC20, address, int256, int256) - ); - return - bentoBox.withdraw( - token, - msg.sender, - to, - _num(amount, value1, value2), - _num(share, value1, value2) - ); + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); } /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. @@ -774,13 +651,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { uint256 value1, uint256 value2 ) internal returns (bytes memory, uint8) { - ( - address callee, - bytes memory callData, - bool useValue1, - bool useValue2, - uint8 returnValues - ) = abi.decode(data, (address, bytes, bool, bool, uint8)); + (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( + data, + (address, bytes, bool, bool, uint8) + ); if (useValue1 && !useValue2) { callData = abi.encodePacked(callData, value1); @@ -790,14 +664,9 @@ contract PrivatePool is BoringOwnable, IMasterContract { callData = abi.encodePacked(callData, value1, value2); } - require( - callee != address(bentoBox) && callee != address(this), - "PrivatePool: can't call" - ); + require(callee != address(bentoBox) && callee != address(this), "PrivatePool: can't call"); - (bool success, bytes memory returnData) = callee.call{value: value}( - callData - ); + (bool success, bytes memory returnData) = callee.call{value: value}(callData); require(success, "PrivatePool: call failed"); return (returnData, returnValues); } @@ -827,123 +696,56 @@ contract PrivatePool is BoringOwnable, IMasterContract { status.hasAccrued = true; } if (action == ACTION_ADD_COLLATERAL) { - (int256 share, address to, bool skim) = abi.decode( - datas[i], - (int256, address, bool) - ); + (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool)); addCollateral(to, skim, _num(share, value1, value2)); } else if (action == ACTION_ADD_ASSET) { - (int256 share, bool skim) = abi.decode( - datas[i], - (int256, bool) - ); + (int256 share, bool skim) = abi.decode(datas[i], (int256, bool)); _addAsset(skim, _num(share, value1, value2)); } else if (action == ACTION_REPAY) { - (int256 part, address to, bool skim) = abi.decode( - datas[i], - (int256, address, bool) - ); + (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool)); _repay(to, skim, _num(part, value1, value2)); } else if (action == ACTION_REMOVE_ASSET) { - (int256 share, address to) = abi.decode( - datas[i], - (int256, address) - ); + (int256 share, address to) = abi.decode(datas[i], (int256, address)); _removeAsset(to, _num(share, value1, value2)); } else if (action == ACTION_REMOVE_COLLATERAL) { - (int256 share, address to) = abi.decode( - datas[i], - (int256, address) - ); + (int256 share, address to) = abi.decode(datas[i], (int256, address)); _removeCollateral(to, _num(share, value1, value2)); status.needsSolvencyCheck = true; } else if (action == ACTION_BORROW) { - (int256 amount, address to) = abi.decode( - datas[i], - (int256, address) - ); + (int256 amount, address to) = abi.decode(datas[i], (int256, address)); (value1, value2) = _borrow(to, _num(amount, value1, value2)); status.needsSolvencyCheck = true; } else if (action == ACTION_UPDATE_EXCHANGE_RATE) { - (bool must_update, uint256 minRate, uint256 maxRate) = abi - .decode(datas[i], (bool, uint256, uint256)); + (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256)); (bool updated, uint256 rate) = updateExchangeRate(); - require( - (!must_update || updated) && - rate > minRate && - (maxRate == 0 || rate > maxRate), - "PrivatePool: rate not ok" - ); + require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), "PrivatePool: rate not ok"); } else if (action == ACTION_BENTO_SETAPPROVAL) { - ( - address user, - address _masterContract, - bool approved, - uint8 v, - bytes32 r, - bytes32 s - ) = abi.decode( - datas[i], - (address, address, bool, uint8, bytes32, bytes32) - ); - bentoBox.setMasterContractApproval( - user, - _masterContract, - approved, - v, - r, - s - ); - } else if (action == ACTION_BENTO_DEPOSIT) { - (value1, value2) = _bentoDeposit( + (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], - values[i], - value1, - value2 + (address, address, bool, uint8, bytes32, bytes32) ); + bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); } else if (action == ACTION_BENTO_WITHDRAW) { (value1, value2) = _bentoWithdraw(datas[i], value1, value2); } else if (action == ACTION_BENTO_TRANSFER) { - (IERC20 token, address to, int256 share) = abi.decode( - datas[i], - (IERC20, address, int256) - ); - bentoBox.transfer( - token, - msg.sender, - to, - _num(share, value1, value2) - ); + (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); + bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { - ( - IERC20 token, - address[] memory tos, - uint256[] memory shares - ) = abi.decode(datas[i], (IERC20, address[], uint256[])); + (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); bentoBox.transferMultiple(token, msg.sender, tos, shares); } else if (action == ACTION_CALL) { - (bytes memory returnData, uint8 returnValues) = _call( - values[i], - datas[i], - value1, - value2 - ); + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); if (returnValues == 1) { (value1) = abi.decode(returnData, (uint256)); } else if (returnValues == 2) { - (value1, value2) = abi.decode( - returnData, - (uint256, uint256) - ); + (value1, value2) = abi.decode(returnData, (uint256, uint256)); } } else if (action == ACTION_GET_REPAY_SHARE) { int256 part = abi.decode(datas[i], (int256)); - value1 = bentoBox.toShare( - asset, - totalDebt.toElastic(_num(part, value1, value2), true), - true - ); + value1 = bentoBox.toShare(asset, totalDebt.toElastic(_num(part, value1, value2), true), true); } else if (action == ACTION_GET_REPAY_PART) { int256 amount = abi.decode(datas[i], (int256)); value1 = totalDebt.toBase(_num(amount, value1, value2), false); @@ -951,10 +753,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { } if (status.needsSolvencyCheck) { - require( - _isSolvent(msg.sender, exchangeRate), - "PrivatePool: borrower insolvent" - ); + require(_isSolvent(msg.sender, exchangeRate), "PrivatePool: borrower insolvent"); } } @@ -974,10 +773,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { accrue(); AccrueInfo memory _accrueInfo = accrueInfo; - require( - block.timestamp >= _accrueInfo.EXPIRATION, - "PrivatePool: no liquidation yet" - ); + require(block.timestamp >= _accrueInfo.EXPIRATION, "PrivatePool: no liquidation yet"); uint256 allCollateralShare; uint256 allDebtAmount; @@ -989,16 +785,11 @@ contract PrivatePool is BoringOwnable, IMasterContract { // If we set an expiration at all, then by the above check it is // now past and every borrower can be liquidated at the current // price: - if ( - (_accrueInfo.EXPIRATION > 0) || - !_isSolvent(borrower, _exchangeRate) - ) { + if ((_accrueInfo.EXPIRATION > 0) || !_isSolvent(borrower, _exchangeRate)) { uint256 debtPart; { uint256 availableDebtPart = borrowerDebtPart[borrower]; - debtPart = maxDebtParts[i] > availableDebtPart - ? availableDebtPart - : maxDebtParts[i]; + debtPart = maxDebtParts[i] > availableDebtPart ? availableDebtPart : maxDebtParts[i]; // No underflow: ensured by definition of debtPart borrowerDebtPart[borrower] = availableDebtPart - debtPart; } @@ -1007,9 +798,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // The exchange rate need not be reasonable: with an expiration // time set there is no _isSolvent() call. uint256 collateralShare = bentoBoxTotals.toBase( - (debtAmount * _accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul( - _exchangeRate - ) / (BPS * EXCHANGE_RATE_PRECISION), + (debtAmount * _accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul(_exchangeRate) / (BPS * EXCHANGE_RATE_PRECISION), false ); @@ -1018,30 +807,13 @@ contract PrivatePool is BoringOwnable, IMasterContract { // "full" liquidation or less). // Underflow check is business logic: the liquidator can only // take enough to cover the loan (and bonus). - userCollateralShare[borrower] = userCollateralShare[borrower] - .sub(collateralShare); + userCollateralShare[borrower] = userCollateralShare[borrower].sub(collateralShare); if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) { - emit LogSeizeCollateral( - borrower, - collateralShare, - debtAmount, - debtPart - ); + emit LogSeizeCollateral(borrower, collateralShare, debtAmount, debtPart); } else { - emit LogRemoveCollateral( - borrower, - swapper == ISimpleSwapper(0) ? to : address(swapper), - collateralShare - ); - emit LogRepay( - swapper == ISimpleSwapper(0) - ? msg.sender - : address(swapper), - borrower, - debtAmount, - debtPart - ); + emit LogRemoveCollateral(borrower, swapper == ISimpleSwapper(0) ? to : address(swapper), collateralShare); + emit LogRepay(swapper == ISimpleSwapper(0) ? msg.sender : address(swapper), borrower, debtAmount, debtPart); } // No overflow in the below three: @@ -1077,8 +849,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // lender, and the fee to favor the liquidator: // Math: All collateral fits in 128 bits (BentoBox), so the // multiplications are safe: - uint256 excessShare = (allCollateralShare * - (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / + uint256 excessShare = (allCollateralShare * (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / _accrueInfo.LIQUIDATION_MULTIPLIER_BPS; uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS; uint256 lenderShare = allCollateralShare - excessShare; @@ -1092,12 +863,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { collateralBalance = _collateralBalance; } userCollateralShare[lender] += lenderShare; - bentoBox.transfer( - collateral, - address(this), - to, - excessShare - feeShare - ); + bentoBox.transfer(collateral, address(this), to, excessShare - feeShare); } else { // No underflow: allCollateralShare is the sum of quantities that // have successfully been taken out of user balances. @@ -1109,19 +875,12 @@ contract PrivatePool is BoringOwnable, IMasterContract { // allDebtAmount <= totalDebt.elastic < 2^128 (proof in loop) // LIQUIDATION_MULTIPLIER_BPS < 2^16 // PROTOCOL_FEE_BPS <= 10k < 2^14 (or we have bigger problems) - uint256 feeAmount = (allDebtAmount * - (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS) * - PROTOCOL_FEE_BPS) / (BPS * BPS); + uint256 feeAmount = (allDebtAmount * (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS) * PROTOCOL_FEE_BPS) / (BPS * BPS); // Swap using a swapper freely chosen by the caller // Open (flash) liquidation: get proceeds first and provide the // borrow after - bentoBox.transfer( - collateral, - address(this), - swapper == ISimpleSwapper(0) ? to : address(swapper), - allCollateralShare - ); + bentoBox.transfer(collateral, address(this), swapper == ISimpleSwapper(0) ? to : address(swapper), allCollateralShare); if (swapper != ISimpleSwapper(0)) { // TODO: Somehow split _receiveAsset to reduce loads? IERC20 _asset = asset; @@ -1129,11 +888,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { collateral, _asset, msg.sender, - bentoBox.toShare( - _asset, - allDebtAmount.add(feeAmount), - true - ), + bentoBox.toShare(_asset, allDebtAmount.add(feeAmount), true), allCollateralShare ); } diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index 338ec192..9dfe85b0 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -37,17 +37,9 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogAddCollateral( - address indexed from, - address indexed to, - uint256 tokenId - ); + event LogAddCollateral(address indexed from, address indexed to, uint256 tokenId); event LogAddAsset(address indexed from, uint256 share); - event LogRemoveCollateral( - address indexed from, - address indexed to, - uint256 tokenId - ); + event LogRemoveCollateral(address indexed from, address indexed to, uint256 tokenId); event LogRemoveAsset(address indexed to, uint256 share); event LogBorrow(address indexed from, address indexed to, uint256 tokenId); event LogRepay(address indexed from, uint256 tokenId); @@ -124,16 +116,10 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { /// @notice De facto constructor for clone contracts function init(bytes calldata data) public payable override { - require( - address(collateral) == address(0), - "PrivatePool: already initialized" - ); + require(address(collateral) == address(0), "PrivatePool: already initialized"); InitSettings memory settings = abi.decode(data, (InitSettings)); - require( - address(settings.collateral) != address(0), - "PrivatePool: bad pair" - ); + require(address(settings.collateral) != address(0), "PrivatePool: bad pair"); collateral = settings.collateral; asset = settings.asset; @@ -148,9 +134,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { } // Enforces that settings are valid - function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) - internal - { + function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) internal { require(params.openFeeBPS < BPS, "PrivatePool: open fee"); tokenLoanParams[tokenId] = params; } @@ -158,9 +142,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { // Enforces that changes only benefit the borrower, if any. // Can be changed, but only in favour of the borrower. This includes giving // them another shot. - function updateLoanParams(uint256 tokenId, TokenLoanParams calldata params) - public - { + function updateLoanParams(uint256 tokenId, TokenLoanParams calldata params) public { require(msg.sender == lender, "PrivatePool: not the lender"); uint8 loanStatus = tokenLoan[tokenId].status; if (loanStatus == LOAN_TAKEN) { @@ -189,14 +171,8 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { require(loanParams.valuation > 0, "PrivatePool: loan unavailable"); if (skim) { - require( - collateral.ownerOf(tokenId) == address(this), - "PrivatePool: skim failed" - ); - require( - tokenLoan[tokenId].status == LOAN_INITIAL, - "PrivatePool: in use" - ); + require(collateral.ownerOf(tokenId) == address(this), "PrivatePool: skim failed"); + require(tokenLoan[tokenId].status == LOAN_INITIAL, "PrivatePool: in use"); } else { collateral.safeTransferFrom(msg.sender, address(this), tokenId); } @@ -230,19 +206,13 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { function removeCollateral(uint256 tokenId, address to) public { TokenLoan memory loan = tokenLoan[tokenId]; if (msg.sender == loan.borrower) { - require( - loan.status == LOAN_COLLATERAL_DEPOSITED, - "PrivatePool: not paid off" - ); + require(loan.status == LOAN_COLLATERAL_DEPOSITED, "PrivatePool: not paid off"); } else { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: require(loan.status == LOAN_TAKEN, "PrivatePool: paid off"); require(msg.sender == lender, "PrivatePool: not the lender"); - require( - tokenLoanParams[tokenId].expiration <= block.timestamp, - "PrivatePool: not expired" - ); + require(tokenLoanParams[tokenId].expiration <= block.timestamp, "PrivatePool: not expired"); } delete tokenLoan[tokenId]; collateral.safeTransferFrom(address(this), to, tokenId); @@ -262,8 +232,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { ) internal { IERC20 _asset = asset; AssetBalance memory _assetBalance = assetBalance; - uint256 priorAssetTotalShare = _assetBalance.reservesShare + - _assetBalance.feesEarnedShare; + uint256 priorAssetTotalShare = _assetBalance.reservesShare + _assetBalance.feesEarnedShare; Rebase memory bentoBoxTotals = bentoBox.totals(_asset); uint256 toFeesShare = 0; @@ -282,13 +251,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { assetBalance = _assetBalance; if (skim) { - require( - takenShare <= - bentoBox.balanceOf(_asset, address(this)).sub( - priorAssetTotalShare - ), - "PrivatePool: skim too much" - ); + require(takenShare <= bentoBox.balanceOf(_asset, address(this)).sub(priorAssetTotalShare), "PrivatePool: skim too much"); } else { bentoBox.transfer(_asset, msg.sender, address(this), takenShare); } @@ -309,9 +272,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { function removeAsset(address to, uint256 share) public { require(msg.sender == lender, "PrivatePool: not the lender"); // Cast safe: Bento transfer reverts unless stronger condition holds - assetBalance.reservesShare = assetBalance.reservesShare.sub( - uint128(share) - ); + assetBalance.reservesShare = assetBalance.reservesShare.sub(uint128(share)); bentoBox.transfer(asset, address(this), to, share); emit LogRemoveAsset(to, share); } @@ -329,11 +290,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { TokenLoan memory loan = tokenLoan[tokenId]; // If you managed to add the collateral, then you are approved. (Even // if we add a method to update the borrower whitelist later..) - require( - loan.status == LOAN_COLLATERAL_DEPOSITED && - loan.borrower == msg.sender, - "PrivatePool: no collateral" - ); + require(loan.status == LOAN_COLLATERAL_DEPOSITED && loan.borrower == msg.sender, "PrivatePool: no collateral"); TokenLoanParams memory params = tokenLoanParams[tokenId]; require(params.expiration > block.timestamp, "PrivatePool: expired"); require( @@ -350,11 +307,9 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint256 protocolFeeShare; { // No overflow: max 128 + 16 bits - uint256 openFeeAmount = (uint256(params.valuation) * - params.openFeeBPS) / BPS; + uint256 openFeeAmount = (uint256(params.valuation) * params.openFeeBPS) / BPS; // No overflow: max 144 + 16 bits - uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / - BPS; + uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS; // No underflow: openFeeBPS < BPS is enforced. amount = params.valuation - openFeeAmount; protocolFeeShare = bentoBoxTotals.toBase(protocolFeeAmount, false); @@ -368,9 +323,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { // results of these calculations: `share` is not modified. // Theoretically the fee could just make it overflow 128 bits. // Underflow check is core business logic: - _assetBalance.reservesShare = _assetBalance.reservesShare.sub( - (share + protocolFeeShare).to128() - ); + _assetBalance.reservesShare = _assetBalance.reservesShare.sub((share + protocolFeeShare).to128()); // Cast is safe: `share` fits. Also, the checked cast above // succeeded. No overflow: protocolFeeShare < reservesShare, and // both balances together fit in the Bento share balance, @@ -476,10 +429,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_TAKEN, "PrivatePool: no loan"); TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - require( - loanParams.expiration > block.timestamp, - "PrivatePool: loan expired" - ); + require(loanParams.expiration > block.timestamp, "PrivatePool: loan expired"); uint256 principal = loanParams.valuation; @@ -528,9 +478,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint256 value1, uint256 value2 ) internal pure returns (uint256 outNum) { - outNum = inNum >= 0 - ? uint256(inNum) - : (inNum == USE_VALUE1 ? value1 : value2); + outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); } /// @dev Helper function for depositing into `bentoBox`. @@ -540,20 +488,10 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint256 value1, uint256 value2 ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode( - data, - (IERC20, address, int256, int256) - ); + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors share = int256(_num(share, value1, value2)); - return - bentoBox.deposit{value: value}( - token, - msg.sender, - to, - uint256(amount), - uint256(share) - ); + return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); } /// @dev Helper function to withdraw from the `bentoBox`. @@ -562,18 +500,8 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint256 value1, uint256 value2 ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode( - data, - (IERC20, address, int256, int256) - ); - return - bentoBox.withdraw( - token, - msg.sender, - to, - _num(amount, value1, value2), - _num(share, value1, value2) - ); + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); } /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. @@ -585,13 +513,10 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint256 value1, uint256 value2 ) internal returns (bytes memory, uint8) { - ( - address callee, - bytes memory callData, - bool useValue1, - bool useValue2, - uint8 returnValues - ) = abi.decode(data, (address, bytes, bool, bool, uint8)); + (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( + data, + (address, bytes, bool, bool, uint8) + ); if (useValue1 && !useValue2) { callData = abi.encodePacked(callData, value1); @@ -601,14 +526,9 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { callData = abi.encodePacked(callData, value1, value2); } - require( - callee != address(bentoBox) && callee != address(this), - "PrivatePool: can't call" - ); + require(callee != address(bentoBox) && callee != address(this), "PrivatePool: can't call"); - (bool success, bytes memory returnData) = callee.call{value: value}( - callData - ); + (bool success, bytes memory returnData) = callee.call{value: value}(callData); require(success, "PrivatePool: call failed"); return (returnData, returnValues); } @@ -628,34 +548,19 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { for (uint256 i = 0; i < actions.length; i++) { uint8 action = actions[i]; if (action == ACTION_ADD_COLLATERAL) { - (uint256 tokenId, address to, bool skim) = abi.decode( - datas[i], - (uint256, address, bool) - ); + (uint256 tokenId, address to, bool skim) = abi.decode(datas[i], (uint256, address, bool)); addCollateral(tokenId, to, skim); } else if (action == ACTION_ADD_ASSET) { - (int256 share, bool skim) = abi.decode( - datas[i], - (int256, bool) - ); + (int256 share, bool skim) = abi.decode(datas[i], (int256, bool)); addAsset(skim, _num(share, value1, value2)); } else if (action == ACTION_REPAY) { - (uint256 tokenId, bool skim) = abi.decode( - datas[i], - (uint256, bool) - ); + (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); repay(tokenId, skim); } else if (action == ACTION_REMOVE_ASSET) { - (int256 share, address to) = abi.decode( - datas[i], - (int256, address) - ); + (int256 share, address to) = abi.decode(datas[i], (int256, address)); removeAsset(to, _num(share, value1, value2)); } else if (action == ACTION_REMOVE_COLLATERAL) { - (uint256 tokenId, address to) = abi.decode( - datas[i], - (uint256, address) - ); + (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); } else if (action == ACTION_BORROW) { ( @@ -665,87 +570,31 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint64 minExpiration, uint16 maxOpenFeeBPS, uint16 maxAnnualInterestBPS - ) = abi.decode( - datas[i], - ( - uint256, - address, - uint128, - uint64, - uint16, - uint16 - ) - ); - (value1, value2) = borrow( - tokenId, - to, - minValuation, - minExpiration, - maxOpenFeeBPS, - maxAnnualInterestBPS - ); + ) = abi.decode(datas[i], (uint256, address, uint128, uint64, uint16, uint16)); + (value1, value2) = borrow(tokenId, to, minValuation, minExpiration, maxOpenFeeBPS, maxAnnualInterestBPS); } else if (action == ACTION_BENTO_SETAPPROVAL) { - ( - address user, - address _masterContract, - bool approved, - uint8 v, - bytes32 r, - bytes32 s - ) = abi.decode( - datas[i], - (address, address, bool, uint8, bytes32, bytes32) - ); - bentoBox.setMasterContractApproval( - user, - _masterContract, - approved, - v, - r, - s - ); - } else if (action == ACTION_BENTO_DEPOSIT) { - (value1, value2) = _bentoDeposit( + (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], - values[i], - value1, - value2 + (address, address, bool, uint8, bytes32, bytes32) ); + bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); } else if (action == ACTION_BENTO_WITHDRAW) { (value1, value2) = _bentoWithdraw(datas[i], value1, value2); } else if (action == ACTION_BENTO_TRANSFER) { - (IERC20 token, address to, int256 share) = abi.decode( - datas[i], - (IERC20, address, int256) - ); - bentoBox.transfer( - token, - msg.sender, - to, - _num(share, value1, value2) - ); + (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); + bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { - ( - IERC20 token, - address[] memory tos, - uint256[] memory shares - ) = abi.decode(datas[i], (IERC20, address[], uint256[])); + (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); bentoBox.transferMultiple(token, msg.sender, tos, shares); } else if (action == ACTION_CALL) { - (bytes memory returnData, uint8 returnValues) = _call( - values[i], - datas[i], - value1, - value2 - ); + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); if (returnValues == 1) { (value1) = abi.decode(returnData, (uint256)); } else if (returnValues == 2) { - (value1, value2) = abi.decode( - returnData, - (uint256, uint256) - ); + (value1, value2) = abi.decode(returnData, (uint256, uint256)); } } } diff --git a/test/PrivatePool.forking.test.ts b/test/PrivatePool.forking.test.ts index 965fd6b8..33d6962b 100644 --- a/test/PrivatePool.forking.test.ts +++ b/test/PrivatePool.forking.test.ts @@ -16,9 +16,9 @@ import { WETH9, USDC } from "./constants.mainnet"; const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; -const WETH_WHALE = '0x6555e1CC97d3cbA6eAddebBCD7Ca51d75771e0B8'; -const USDC_WHALE = '0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503'; -const GENERAL_WHALE = '0x84D34f4f83a87596Cd3FB6887cFf8F17Bf5A7B83'; +const WETH_WHALE = "0x6555e1CC97d3cbA6eAddebBCD7Ca51d75771e0B8"; +const USDC_WHALE = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503"; +const GENERAL_WHALE = "0x84D34f4f83a87596Cd3FB6887cFf8F17Bf5A7B83"; const initTypes = { collateral: "address", @@ -53,7 +53,6 @@ const getSignerFor = async (addr) => { }; describe("Private Lending Pool - Forked Mainnet", async () => { - if (process.env.FORKING !== 'true') { return; } let snapshotId; let masterContract: PrivatePool; let bentoBox: BentoBoxV1; @@ -62,7 +61,6 @@ describe("Private Lending Pool - Forked Mainnet", async () => { let usdcWhale: Signer; let generalWhale: Signer; - const deployPair = async (initSettings) => { const deployTx = await bentoBox .deploy(masterContract.address, encodeInitData(initSettings), false) From d040039d702b57f6e94007b35fad617265c6d633 Mon Sep 17 00:00:00 2001 From: 0xCalibur <0xCalibur@protonmail.com> Date: Mon, 24 Jan 2022 16:00:23 -0500 Subject: [PATCH 020/107] add a way to enable/disable approved borrowers --- contracts/PrivatePool.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 346144ca..607477aa 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -165,6 +165,10 @@ contract PrivatePool is BoringOwnable, IMasterContract { } } + function setApprovedBorrowers(address borrower, bool approved) external onlyOwner { + approvedBorrowers[borrower] = approved; + } + /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees. function accrue() public { AccrueInfo memory _accrueInfo = accrueInfo; From a2194abd16b25870b675015d37dd43df88903ab4 Mon Sep 17 00:00:00 2001 From: 0xCalibur <0xCalibur@protonmail.com> Date: Mon, 24 Jan 2022 22:44:18 -0500 Subject: [PATCH 021/107] add a way to change approvedBorrowers and verify approvedBorrowers when borrowing --- contracts/PrivatePoolNFT.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index 9dfe85b0..2b4ce93f 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -133,6 +133,10 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { } } + function setApprovedBorrowers(address borrower, bool approved) external onlyOwner { + approvedBorrowers[borrower] = approved; + } + // Enforces that settings are valid function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) internal { require(params.openFeeBPS < BPS, "PrivatePool: open fee"); @@ -287,6 +291,8 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint16 maxOpenFeeBPS, uint16 maxAnnualInterestBPS ) public returns (uint256 share, uint256 amount) { + require(approvedBorrowers[msg.sender], "PrivatePool: unapproved borrower"); + TokenLoan memory loan = tokenLoan[tokenId]; // If you managed to add the collateral, then you are approved. (Even // if we add a method to update the borrower whitelist later..) From 744bf7c7ec1de09489b996c388a4d99fbbbc7440 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 8 Jan 2022 04:25:12 +1100 Subject: [PATCH 022/107] (Pull in new BoringSolidity with ERC721) --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6247279e..3e7f16d5 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "*.md": "prettier --write" }, "devDependencies": { - "@boringcrypto/boring-solidity": "boringcrypto/BoringSolidity#51616f5bbab53adaf6379428de08c9d8093c63f5", + "@boringcrypto/boring-solidity": "boringcrypto/BoringSolidity#ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc", "@commitlint/cli": "^13.2.1", "@commitlint/config-conventional": "^13.2.0", "@nomiclabs/hardhat-ethers": "yarn:hardhat-deploy-ethers", diff --git a/yarn.lock b/yarn.lock index 1a65d606..47fb2b44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,9 +27,9 @@ version "1.2.1" resolved "https://codeload.github.com/boringcrypto/BoringSolidity/tar.gz/371a01c11465eb1c881193c60a53c0bf15ee8a19" -"@boringcrypto/boring-solidity@boringcrypto/BoringSolidity#51616f5bbab53adaf6379428de08c9d8093c63f5": - version "1.2.2" - resolved "https://codeload.github.com/boringcrypto/BoringSolidity/tar.gz/51616f5bbab53adaf6379428de08c9d8093c63f5" +"@boringcrypto/boring-solidity@boringcrypto/BoringSolidity#ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc": + version "1.2.4" + resolved "https://codeload.github.com/boringcrypto/BoringSolidity/tar.gz/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc" "@commitlint/cli@^13.2.1": version "13.2.1" From 997b59d21bfb705ccb08191c33484751de4e37fb Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 26 Jan 2022 03:32:03 +1100 Subject: [PATCH 023/107] (Format all .ts files with "yarn format") --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e7f16d5..ff27e21f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "boba:verify": "hardhat sourcify --network boba", "moonriver:deploy": "hardhat --network moonriver deploy", "moonriver:verify": "hardhat --network moonriver etherscan-verify --solc-input --license GPL-3.0 --force-license", - "format": "prettier --write contracts/**/*.sol *.js *.json test/**/*.js", + "format": "prettier --write contracts/**/*.sol *.js *.json test/**/*.js *.ts test/**/*.ts", "pretty-quick": "pretty-quick", "test": "cross-env TS_NODE_TRANSPILE_ONLY=1 hardhat test", "test:coverage": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" hardhat coverage", From 3d7a45c9c64ecce514efcac9661fd6a4a616978b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 26 Jan 2022 03:45:15 +1100 Subject: [PATCH 024/107] Refactor removeCollateral --- contracts/PrivatePoolNFT.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index 2b4ce93f..3746c3c4 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -209,18 +209,19 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { /// @param tokenId The token function removeCollateral(uint256 tokenId, address to) public { TokenLoan memory loan = tokenLoan[tokenId]; - if (msg.sender == loan.borrower) { - require(loan.status == LOAN_COLLATERAL_DEPOSITED, "PrivatePool: not paid off"); + if (loan.status == LOAN_COLLATERAL_DEPOSITED) { + // We are withdrawing collateral that is not in use: + require(msg.sender == loan.borrower, "PrivatePool: not the borrower"); } else { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: - require(loan.status == LOAN_TAKEN, "PrivatePool: paid off"); require(msg.sender == lender, "PrivatePool: not the lender"); + require(loan.status == LOAN_TAKEN, "PrivatePool: paid off"); require(tokenLoanParams[tokenId].expiration <= block.timestamp, "PrivatePool: not expired"); } + emit LogRemoveCollateral(loan.borrower, to, tokenId); delete tokenLoan[tokenId]; collateral.safeTransferFrom(address(this), to, tokenId); - emit LogRemoveCollateral(loan.borrower, to, tokenId); } /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. From 630e3d16b730fa08e3a605e9c3d53d157a46de10 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 26 Jan 2022 04:12:45 +1100 Subject: [PATCH 025/107] Make "compound interest terms" a per-loan setting --- contracts/PrivatePoolNFT.sol | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index 3746c3c4..e6b14e84 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -79,6 +79,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { uint64 expiration; // Pay before this or get liquidated uint16 openFeeBPS; // Fixed cost of taking out the loan uint16 annualInterestBPS; // Variable cost of taking out the loan + uint8 compoundInterestTerms; // Might as well. Stay under 50. } mapping(uint256 => TokenLoanParams) public tokenLoanParams; @@ -92,8 +93,6 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { } mapping(uint256 => TokenLoan) public tokenLoan; - // TODO: Configurable per loan? - uint256 private constant COMPOUND_INTEREST_TERMS = 4; // Do not go over 50. uint256 private constant PROTOCOL_FEE_BPS = 1000; // Do not go over 100%.. uint256 private constant BPS = 10_000; uint256 private constant YEAR = 3600 * 24 * 365; @@ -355,7 +354,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { /// may be very inaccurate. Which does not matter, because the BentoBox /// cannot hold that high a balance. /// - /// @param n Highest term. Set n=1 for linear (non-compound) interest. + /// @param n Highest order term. Set n=1 (or 0) for linear interest only. function calculateInterest( uint256 principal, uint256 t, @@ -369,7 +368,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { // t = 30,000 years (40 bits) // interest = 655.35% APR (16 bits) // - // Even then, we will not see an overflow until the fifth term: + // Even then, we will not see an overflow until after the fifth term: // // k denom > 2^ term * x <= 2^ term * x / denom <= 2^ // --------------------------------------------------------------- @@ -445,7 +444,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { principal, block.timestamp - loan.startTime, loanParams.annualInterestBPS, - COMPOUND_INTEREST_TERMS + loanParams.compoundInterestTerms ).to128(); // No overflow (both lines): to128() would have reverted amount = principal + interest; From 49b7342fa2352e50c54359703ec480397d39a8dd Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 26 Jan 2022 04:12:46 +1100 Subject: [PATCH 026/107] Borrow: Pass loan params as struct --- contracts/PrivatePoolNFT.sol | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index e6b14e84..be1ff144 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -145,7 +145,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { // Enforces that changes only benefit the borrower, if any. // Can be changed, but only in favour of the borrower. This includes giving // them another shot. - function updateLoanParams(uint256 tokenId, TokenLoanParams calldata params) public { + function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { require(msg.sender == lender, "PrivatePool: not the lender"); uint8 loanStatus = tokenLoan[tokenId].status; if (loanStatus == LOAN_TAKEN) { @@ -286,10 +286,7 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { function borrow( uint256 tokenId, address to, - uint128 minValuation, - uint64 minExpiration, - uint16 maxOpenFeeBPS, - uint16 maxAnnualInterestBPS + TokenLoanParams memory offered ) public returns (uint256 share, uint256 amount) { require(approvedBorrowers[msg.sender], "PrivatePool: unapproved borrower"); @@ -299,11 +296,15 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { require(loan.status == LOAN_COLLATERAL_DEPOSITED && loan.borrower == msg.sender, "PrivatePool: no collateral"); TokenLoanParams memory params = tokenLoanParams[tokenId]; require(params.expiration > block.timestamp, "PrivatePool: expired"); + + // Valuation has to be an exact match, everything else must be at least + // as cheap as promised: require( - params.valuation >= minValuation && - params.expiration >= minExpiration && - params.openFeeBPS <= maxOpenFeeBPS && - params.annualInterestBPS <= maxAnnualInterestBPS, + params.valuation == offered.valuation && + params.expiration >= offered.expiration && + params.openFeeBPS <= offered.openFeeBPS && + params.annualInterestBPS <= offered.annualInterestBPS && + params.compoundInterestTerms <= offered.compoundInterestTerms, "PrivatePool: bad params" ); @@ -569,15 +570,8 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); } else if (action == ACTION_BORROW) { - ( - uint256 tokenId, - address to, - uint128 minValuation, - uint64 minExpiration, - uint16 maxOpenFeeBPS, - uint16 maxAnnualInterestBPS - ) = abi.decode(datas[i], (uint256, address, uint128, uint64, uint16, uint16)); - (value1, value2) = borrow(tokenId, to, minValuation, minExpiration, maxOpenFeeBPS, maxAnnualInterestBPS); + (uint256 tokenId, address to, TokenLoanParams memory offered) = abi.decode(datas[i], (uint256, address, TokenLoanParams)); + (value1, value2) = borrow(tokenId, to, offered); } else if (action == ACTION_BENTO_SETAPPROVAL) { (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], From a74f96fb51f5cf714980fc527464f78339710853 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 26 Jan 2022 04:12:48 +1100 Subject: [PATCH 027/107] Emit event on updating loan params --- contracts/PrivatePoolNFT.sol | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/contracts/PrivatePoolNFT.sol b/contracts/PrivatePoolNFT.sol index be1ff144..e805db22 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/PrivatePoolNFT.sol @@ -45,6 +45,14 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { event LogRepay(address indexed from, uint256 tokenId); event LogFeeTo(address indexed newFeeTo); event LogWithdrawFees(address indexed feeTo, uint256 feeShare); + event LogUpdateLoanParams( + uint256 tokenId, + uint128 valuation, + uint64 expiration, + uint16 openFeeBPS, + uint16 annualInterestBPS, + uint8 compoundInterestTerms + ); // Immutables (for MasterContract and all clones) IBentoBoxV1 public immutable bentoBox; @@ -140,6 +148,14 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) internal { require(params.openFeeBPS < BPS, "PrivatePool: open fee"); tokenLoanParams[tokenId] = params; + emit LogUpdateLoanParams( + tokenId, + params.valuation, + params.expiration, + params.openFeeBPS, + params.annualInterestBPS, + params.compoundInterestTerms + ); } // Enforces that changes only benefit the borrower, if any. @@ -149,11 +165,12 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { require(msg.sender == lender, "PrivatePool: not the lender"); uint8 loanStatus = tokenLoan[tokenId].status; if (loanStatus == LOAN_TAKEN) { - TokenLoanParams memory current = tokenLoanParams[tokenId]; + TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( - params.expiration >= current.expiration && - params.valuation <= current.valuation && - params.annualInterestBPS <= current.annualInterestBPS, + params.expiration >= cur.expiration && + params.valuation <= cur.valuation && + params.annualInterestBPS <= cur.annualInterestBPS && + params.compoundInterestTerms <= cur.compoundInterestTerms, "PrivatePool: worse params" ); } From 79e192bb457bc28a77d2d57b5d07560f1467f520 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 1 Feb 2022 03:21:11 +1100 Subject: [PATCH 028/107] Add some tests for the NFT contract --- contracts/mocks/ERC721Mock.sol | 14 + test/PrivatePool.forking.test.ts | 34 +- test/PrivatePool.test.ts | 411 +++++---------------- test/PrivatePool.ts | 84 ++++- test/PrivatePoolNFT.test.ts | 588 +++++++++++++++++++++++++++++++ test/helpers.ts | 25 ++ tsconfig.json | 2 +- 7 files changed, 797 insertions(+), 361 deletions(-) create mode 100644 contracts/mocks/ERC721Mock.sol create mode 100644 test/PrivatePoolNFT.test.ts create mode 100644 test/helpers.ts diff --git a/contracts/mocks/ERC721Mock.sol b/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000..c236608a --- /dev/null +++ b/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +import "@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol"; + +contract ERC721Mock is BoringMultipleNFT { + function mint(address owner) public returns (uint256 id) { + id = totalSupply; + _mint(owner, 0); + } + + function _tokenURI(uint256) internal view override returns (string memory) { + return ""; + } +} diff --git a/test/PrivatePool.forking.test.ts b/test/PrivatePool.forking.test.ts index 33d6962b..a56d02c6 100644 --- a/test/PrivatePool.forking.test.ts +++ b/test/PrivatePool.forking.test.ts @@ -1,10 +1,4 @@ -import { - ethers, - network, - deployments, - getNamedAccounts, - artifacts, -} from "hardhat"; +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; import { expect } from "chai"; import { BigNumberish, Signer } from "ethers"; import _ from "lodash"; @@ -42,10 +36,7 @@ const typeDefaults = { // These rely on JS/TS iterating over the keys in the order they were defined: const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); const encodeInitData = (kvs) => - ethers.utils.defaultAbiCoder.encode( - [`tuple(${initTypeString})`], - [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] - ); + ethers.utils.defaultAbiCoder.encode([`tuple(${initTypeString})`], [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)]); const getSignerFor = async (addr) => { await impersonate(addr); @@ -62,13 +53,9 @@ describe("Private Lending Pool - Forked Mainnet", async () => { let generalWhale: Signer; const deployPair = async (initSettings) => { - const deployTx = await bentoBox - .deploy(masterContract.address, encodeInitData(initSettings), false) - .then((tx) => tx.wait()); + const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal( - "LogDeploy(address,bytes,address)" - ); + expect(deployEvent.eventSignature).to.equal("LogDeploy(address,bytes,address)"); const { cloneAddress } = deployEvent.args; return ethers.getContractAt("PrivatePool", cloneAddress); }; @@ -80,9 +67,7 @@ describe("Private Lending Pool - Forked Mainnet", async () => { params: [ { forking: { - jsonRpcUrl: - process.env.ETHEREUM_RPC_URL || - `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, + jsonRpcUrl: process.env.ETHEREUM_RPC_URL || `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, blockNumber: 13715035, }, }, @@ -91,10 +76,7 @@ describe("Private Lending Pool - Forked Mainnet", async () => { await deployments.fixture(["PrivatePool"]); masterContract = await ethers.getContract("PrivatePool"); - bentoBox = await ethers.getContractAt( - "BentoBoxV1", - await masterContract.bentoBox() - ); + bentoBox = await ethers.getContractAt("BentoBoxV1", await masterContract.bentoBox()); const sevenPercentAnnually = getBigNumber(7).div(100 * 3600 * 24 * 365); pairContract = await deployPair({ @@ -153,9 +135,7 @@ describe("Private Lending Pool - Forked Mainnet", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( - "PrivatePool: already initialized" - ); + await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); }); }); }); diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index 73039422..2beb5dae 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -1,27 +1,9 @@ -import { - ethers, - network, - deployments, - getNamedAccounts, - artifacts, -} from "hardhat"; +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; import { expect } from "chai"; import { BigNumberish, Signer } from "ethers"; -import { - advanceNextTime, - duration, - encodeParameters, - getBigNumber, - impersonate, -} from "../utilities"; -import { - BentoBoxMock, - ERC20Mock, - OracleMock, - WETH9Mock, - PrivatePool, -} from "../typechain"; +import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; +import { BentoBoxMock, ERC20Mock, OracleMock, WETH9Mock, PrivatePool } from "../typechain"; import { encodeInitData } from "./PrivatePool"; const { formatUnits } = ethers.utils; @@ -102,13 +84,9 @@ describe("Private Lending Pool", async () => { }); const deployPair = async (initSettings) => { - const deployTx = await bentoBox - .deploy(masterContract.address, encodeInitData(initSettings), false) - .then((tx) => tx.wait()); + const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal( - "LogDeploy(address,bytes,address)" - ); + expect(deployEvent.eventSignature).to.equal("LogDeploy(address,bytes,address)"); const { cloneAddress } = deployEvent.args; return ethers.getContractAt("PrivatePool", cloneAddress); }; @@ -123,20 +101,10 @@ describe("Private Lending Pool", async () => { ["Guineas", guineas], ]) { const balance = await contract.balanceOf(acc.address); - const bentoShares = await bentoBox.balanceOf( - contract.address, - acc.address - ); - const bentoBalance = await bentoBox.toAmount( - contract.address, - bentoShares, - false - ); + const bentoShares = await bentoBox.balanceOf(contract.address, acc.address); + const bentoBalance = await bentoBox.toAmount(contract.address, bentoShares, false); console.log(`${token}: ${formatUnits(balance)}`); - console.log( - `${token} (BentoBox):`, - `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n` - ); + console.log(`${token} (BentoBox):`, `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n`); } } }; @@ -263,9 +231,7 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith( - "PrivatePool: already initialized" - ); + await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); }); }); @@ -289,9 +255,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(mainPair.connect(alice).addAsset(true, share)) - .to.emit(mainPair, "LogAddAsset") - .withArgs(bentoBox.address, share); + await expect(mainPair.connect(alice).addAsset(true, share)).to.emit(mainPair, "LogAddAsset").withArgs(bentoBox.address, share); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); @@ -309,10 +273,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; const datas = [ - encodeParameters( - ["address", "address", "uint256", "uint256"], - [g, a, amount, 0] - ), + encodeParameters(["address", "address", "uint256", "uint256"], [g, a, amount, 0]), encodeParameters(["int256", "bool"], [share, false]), ]; const values = [0, 0]; @@ -340,9 +301,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect( - mainPair.connect(alice).addAsset(true, share.add(1)) - ).to.be.revertedWith("PrivatePool: skim too much"); + await expect(mainPair.connect(alice).addAsset(true, share.add(1))).to.be.revertedWith("PrivatePool: skim too much"); }); it("Should let anyone add assets", async () => { @@ -410,9 +369,7 @@ describe("Private Lending Pool", async () => { it("Should refuse collateral for unapproved borrowers", async () => { const share = getBigNumber(55); const to = alice.address; - await expect( - mainPair.connect(bob).addCollateral(to, false, share) - ).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect(mainPair.connect(bob).addCollateral(to, false, share)).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should let approved borrowers add collateral (skim)", async () => { @@ -473,9 +430,7 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to lend to unapproved borrowers", async () => { - await expect( - mainPair.connect(alice).borrow(alice.address, 1) - ).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect(mainPair.connect(alice).borrow(alice.address, 1)).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should enforce LTV requirements when borrowing", async () => { @@ -487,21 +442,14 @@ describe("Private Lending Pool", async () => { const collatAmount = collatShare1.mul(531).div(700); // WETH ratio const borrowAmount = collatAmount.mul(9); // 75% of 12 - await expect( - mainPair.connect(bob).borrow(bob.address, borrowAmount) - ).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.be.revertedWith("PrivatePool: borrower insolvent"); // Accounting for the 0.1% open fee is enough to make it succeed: const withFee = borrowAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit( - mainPair, - "LogBorrow" - ); + await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit(mainPair, "LogBorrow"); // Borrowing even one more wei is enough to make it fail again: - await expect( - mainPair.connect(bob).borrow(bob.address, withFee.add(1)) - ).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect(mainPair.connect(bob).borrow(bob.address, withFee.add(1))).to.be.revertedWith("PrivatePool: borrower insolvent"); }); it("Should collect the protocol fee immediately", async () => { @@ -521,9 +469,7 @@ describe("Private Lending Pool", async () => { const protocolFeeShare = protocolFee.mul(9).div(20); const takenShare = borrowShare.add(protocolFeeShare); - await expect( - mainPair.connect(bob).borrow(bob.address, borrowAmount) - ).to.emit(mainPair, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.emit(mainPair, "LogBorrow"); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(assetShare.sub(takenShare)); @@ -535,35 +481,24 @@ describe("Private Lending Pool", async () => { it("Should not lend out more than there is", async () => { // There are 1000 guineas of assets; 150 WETH allows for borrowing // 1350 and is therefore enough: - await mainPair - .connect(bob) - .addCollateral(bob.address, false, getBigNumber(150)); + await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); // More than reserves - await expect( - mainPair.connect(bob).borrow(bob.address, getBigNumber(1001)) - ).to.be.revertedWith("BoringMath: Underflow"); + await expect(mainPair.connect(bob).borrow(bob.address, getBigNumber(1001))).to.be.revertedWith("BoringMath: Underflow"); }); it("Should not defer the protocol fee on new loans", async () => { // This amounts to testing that the amount + protocol fee need to be in // reserve: - await mainPair - .connect(bob) - .addCollateral(bob.address, false, getBigNumber(150)); + await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); // Exactly the reserves (our test amounts divide cleanly), but that is // not enough with the fee: const reservesAmount = getBigNumber(1000); - await expect( - mainPair.connect(bob).borrow(bob.address, reservesAmount) - ).to.be.revertedWith("BoringMath: Underflow"); + await expect(mainPair.connect(bob).borrow(bob.address, reservesAmount)).to.be.revertedWith("BoringMath: Underflow"); const cutoff = reservesAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit( - mainPair, - "LogBorrow" - ); + await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit(mainPair, "LogBorrow"); }); }); @@ -604,20 +539,13 @@ describe("Private Lending Pool", async () => { await advanceNextTime(YEAR); const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = debtAmount1 - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(YEAR) - .div(getBigNumber(1)); + const extraAmount = debtAmount1.mul(MainTestSettings.INTEREST_PER_SECOND).mul(YEAR).div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()) - .to.emit(mainPair, "LogAccrue") - .withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); // Protocol cut of the open fee + fee on interest, both in shares: - expect((await mainPair.assetBalance()).feesEarnedShare).to.equal( - openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20)) - ); + expect((await mainPair.assetBalance()).feesEarnedShare).to.equal(openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20))); expect(await mainPair.feesOwedAmount()).to.equal(0); const totalDebt = await mainPair.totalDebt(); @@ -628,17 +556,13 @@ describe("Private Lending Pool", async () => { // all the rounding, this should be roughly 0.07007 times the amount // taken out. Since the contract rounds down, we expect to be under, so // round up for the test: - expect( - extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1) - ).to.equal(7007); + expect(extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1)).to.equal(7007); }); it("Should not do anything if nothing is borrowed", async () => { await mainPair.accrue(); // No "LogAccrue" event. Cleaner way to do this? - expect( - await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }]) - ).to.deep.equal([]); + expect(await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }])).to.deep.equal([]); }); it("Should defer fees if everything is loaned out", async () => { @@ -648,9 +572,7 @@ describe("Private Lending Pool", async () => { const almostEverything = assetAmount.mul(99).div(100); const openFee = almostEverything.div(1000); const openProtocolFeeShare = openFee.div(10).mul(9).div(20); - const remainingShare = assetShare - .sub(almostEverything.mul(9).div(20)) - .sub(openProtocolFeeShare); + const remainingShare = assetShare.sub(almostEverything.mul(9).div(20)).sub(openProtocolFeeShare); const initialDebt = almostEverything.add(openFee); await mainPair.connect(bob).borrow(bob.address, almostEverything); @@ -660,14 +582,9 @@ describe("Private Lending Pool", async () => { // 7000% interest; the fee is 7% of the initial debt, which is more than // remaining asset reserves. It should still work: const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = initialDebt - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(time) - .div(getBigNumber(1)); + const extraAmount = initialDebt.mul(MainTestSettings.INTEREST_PER_SECOND).mul(time).div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()) - .to.emit(mainPair, "LogAccrue") - .withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); // Outstanding debt is recorded normally: const totalDebt = await mainPair.totalDebt(); @@ -678,9 +595,7 @@ describe("Private Lending Pool", async () => { // These already included the protocol fee: const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(0); - expect(assetBalance.feesEarnedShare).to.equal( - remainingShare.add(openProtocolFeeShare) - ); + expect(assetBalance.feesEarnedShare).to.equal(remainingShare.add(openProtocolFeeShare)); // We collected the remaining asset reserves as fee. The rest is owed: const feeShare = feeAmount.mul(9).div(20); @@ -716,18 +631,12 @@ describe("Private Lending Pool", async () => { // with "seize collateral"-type liquidations; then it's the lender. This // may change if we allow modifying the whitelist; then we'll have to // cleanly handle no-longer-whitelisted users. - expect( - await mainPair.connect(bob).removeCollateral(bob.address, collatShare1) - ) + expect(await mainPair.connect(bob).removeCollateral(bob.address, collatShare1)) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(bob.address, bob.address, collatShare1); const remainder = getBigNumber(12); - expect( - await mainPair - .connect(carol) - .removeCollateral(carol.address, collatShare2.sub(remainder)) - ) + expect(await mainPair.connect(carol).removeCollateral(carol.address, collatShare2.sub(remainder))) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(carol.address, carol.address, collatShare2.sub(remainder)); }); @@ -784,10 +693,7 @@ describe("Private Lending Pool", async () => { expect(await mainPair.borrowerDebtPart(bob.address)).to.equal(debtPart); await advanceNextTime(timeStep); - const extraAmount = debtAmount - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(timeStep) - .div(one); + const extraAmount = debtAmount.mul(MainTestSettings.INTEREST_PER_SECOND).mul(timeStep).div(one); debtAmount = debtAmount.add(extraAmount); // "parts" are in units of the initial debt. These should cover it: @@ -796,10 +702,7 @@ describe("Private Lending Pool", async () => { // Bob owns all the debt, so this is the conversion. Rounding is up, in // favour of the contract, so that the amount definitely covers the // part intended to be paid back: - const repayAmount = repayPart - .mul(debtAmount) - .add(debtPart.sub(1)) - .div(debtPart); + const repayAmount = repayPart.mul(debtAmount).add(debtPart.sub(1)).div(debtPart); // "Smallest number of shares covering this" -- so rounded up again: // Note that -- as in the UniV2 AMMs, for instance -- this number of @@ -841,17 +744,11 @@ describe("Private Lending Pool", async () => { }; t0.bobDebtPart = t0.bobDebtAmount; - expect(t0.assetBalance.reservesShare).to.equal( - assetShare - .sub(bobLoanAmount.mul(9).div(20)) - .sub(bobLoanAmount.div(10_000).mul(9).div(20)) - ); + expect(t0.assetBalance.reservesShare).to.equal(assetShare.sub(bobLoanAmount.mul(9).div(20)).sub(bobLoanAmount.div(10_000).mul(9).div(20))); expect(t0.totalDebt.elastic).to.equal(t0.bobDebtAmount); const t1 = { - accruedInterest: t0.bobDebtAmount - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), + accruedInterest: t0.bobDebtAmount.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), }; // We find the number of shares Carol should borrow to drain reserves. We // account for the protocol cut of Bob's accrued interest, and Carol's @@ -861,11 +758,7 @@ describe("Private Lending Pool", async () => { // of wiggle room (here and elsewhere); "multiplication and then division // with rounding" is not an invertible operation, and not every target // can be reached no matter how you round. (Try (M * 2) / 1 == 3). - const carolLoanShare = t0.assetBalance.reservesShare - .sub(t1.accruedInterest.div(10).mul(9).div(20)) - .add(1) - .mul(10_000) - .div(10_001); + const carolLoanShare = t0.assetBalance.reservesShare.sub(t1.accruedInterest.div(10).mul(9).div(20)).add(1).mul(10_000).div(10_001); const carolLoanAmount = carolLoanShare.mul(20).div(9).add(2); await advanceNextTime(timeStep); @@ -885,9 +778,7 @@ describe("Private Lending Pool", async () => { await mainPair.accrue(); const t2 = { - accruedInterest: t1.totalDebt.elastic - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), + accruedInterest: t1.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), assetBalance: await mainPair.assetBalance(), totalDebt: await mainPair.totalDebt(), feesOwedAmount: await mainPair.feesOwedAmount(), @@ -898,22 +789,16 @@ describe("Private Lending Pool", async () => { // the error is more than 1 or even `toAmount(1)`: expect(t2.assetBalance.reservesShare).to.equal(0); expect(t2.feesOwedAmount).to.be.gt(0); - expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte( - 5 - ); + expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte(5); // Repaying triggers another accrual, so we (advance a fixed time and) // determine how much will be owed after that: const t3 = { - accruedInterest: t2.totalDebt.elastic - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), + accruedInterest: t2.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), }; // Intermediate value; we would see it if we did something that accrues // but not deposits any assets: - t3.feesOwedBeforeRepay = t2.feesOwedAmount.add( - t3.accruedInterest.div(10) - ); + t3.feesOwedBeforeRepay = t2.feesOwedAmount.add(t3.accruedInterest.div(10)); // A debt "part" corresponds to 1 token when the first loan is taken out. // When interest accrues this amount grows correspondingly; it never @@ -937,11 +822,7 @@ describe("Private Lending Pool", async () => { // taken out of reserves, they were not "earned" until we made the // repayment. At which point we expect "fees earned" to have increased // by exactly that amount (in shares): - expect(t3.assetBalance.feesEarnedShare).to.equal( - t2.assetBalance.feesEarnedShare.add( - t3.feesOwedBeforeRepay.mul(9).div(20) - ) - ); + expect(t3.assetBalance.feesEarnedShare).to.equal(t2.assetBalance.feesEarnedShare.add(t3.feesOwedBeforeRepay.mul(9).div(20))); expect(t3.feesOwedAmount).to.equal(0); // Debt gets paid off as normal; in particular the fees are not added to @@ -951,16 +832,11 @@ describe("Private Lending Pool", async () => { // The small amount we repaid in excess of the fees owed should have gone // to asset reserves. - const excessRepayAmount = repayPart - .mul(t3.totalDebt.elastic) - .div(t3.totalDebt.base) - .sub(t3.feesOwedBeforeRepay); + const excessRepayAmount = repayPart.mul(t3.totalDebt.elastic).div(t3.totalDebt.base).sub(t3.feesOwedBeforeRepay); const excessRepayShare = excessRepayAmount.mul(9).div(20); // Again, giving it a few wei of leeway due to rounding in _receiveAsset: - expect( - t3.assetBalance.reservesShare.sub(excessRepayShare).abs() - ).to.be.lte(5); + expect(t3.assetBalance.reservesShare.sub(excessRepayShare).abs()).to.be.lte(5); }); }); @@ -985,9 +861,7 @@ describe("Private Lending Pool", async () => { const [a, b, c] = [alice, bob, carol].map((x) => x.address); await mainPair.connect(alice).addAsset(false, assetShare); await mainPair.connect(bob).addCollateral(b, false, bobCollateralShare); - await mainPair - .connect(carol) - .addCollateral(c, false, carolCollateralShare); + await mainPair.connect(carol).addCollateral(c, false, carolCollateralShare); await oracle.set(one.div(10)); // one WETH is 10 guineas await mainPair.updateExchangeRate(); @@ -1014,14 +888,9 @@ describe("Private Lending Pool", async () => { it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect( - mainPair.liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.be.revertedWith("PrivatePool: all are solvent"); + await expect(mainPair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( + "PrivatePool: all are solvent" + ); }); it("Should liquidate insolvent borrowers only", async () => { @@ -1036,16 +905,7 @@ describe("Private Lending Pool", async () => { // Alice has guineas and has approved the contract. That she is also the // lender makes no difference in the execution path taken. - await expect( - mainPair - .connect(alice) - .liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ) + await expect(mainPair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)) .to.emit(mainPair, "LogRemoveCollateral") .to.emit(mainPair, "LogRepay"); @@ -1060,10 +920,7 @@ describe("Private Lending Pool", async () => { assetBalance: await mainPair.assetBalance(), - aliceBentoGuineas: await bentoBox.balanceOf( - guineas.address, - alice.address - ), + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -1088,45 +945,28 @@ describe("Private Lending Pool", async () => { const minRepayShare = bobLiquidatePart.mul(9).div(20); // Not entirely accurate because it gets calculated differently, but // equivalent up to rounding effects: - const protocolFeeShare = minRepayShare - .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) - .div(10_000) - .sub(minRepayShare) - .div(10); - const aliceMaxBentoGuineas = t0.aliceBentoGuineas - .sub(minRepayShare) - .sub(protocolFeeShare); + const protocolFeeShare = minRepayShare.mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS).div(10_000).sub(minRepayShare).div(10); + const aliceMaxBentoGuineas = t0.aliceBentoGuineas.sub(minRepayShare).sub(protocolFeeShare); expect(t1.aliceBentoGuineas).to.be.lte(aliceMaxBentoGuineas); - expect(t1.aliceBentoGuineas).to.be.gte( - aliceMaxBentoGuineas.mul(9999).div(10_000) - ); + expect(t1.aliceBentoGuineas).to.be.gte(aliceMaxBentoGuineas.mul(9999).div(10_000)); - const aliceMinBentoWeth = t0.aliceBentoWeth.add( - bobMinCollateralTakenShare - ); + const aliceMinBentoWeth = t0.aliceBentoWeth.add(bobMinCollateralTakenShare); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte( - aliceMinBentoWeth.mul(10_001).div(10_000) - ); + expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); // If we want a firm lower bound on asset reserves, we need to account // for interest: the accrue() call right before liquidations charges // interest, and takes the protocol cut of that interest out of reserves. // We divide rounding up: - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( - t1.blockTimestamp - t0.blockTimestamp - ) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = t0.assetBalance.reservesShare - .add(minRepayShare) - .sub(maxInterestFee); - const minFeesEarnedShare = - t0.assetBalance.feesEarnedShare.add(protocolFeeShare); + const minAssetReserves = t0.assetBalance.reservesShare.add(minRepayShare).sub(maxInterestFee); + const minFeesEarnedShare = t0.assetBalance.feesEarnedShare.add(protocolFeeShare); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); expect(t1.assetBalance.feesEarnedShare).to.be.gte(minFeesEarnedShare); @@ -1136,21 +976,13 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal( - t0.totalDebt.base.sub(bobLiquidatePart) - ); + expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); - const bobMaxCollateralShare = t0.bobCollateralShare.sub( - bobMinCollateralTakenShare - ); + const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte( - bobMaxCollateralShare.mul(9999).div(10_000) - ); + expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); // Equivalent check.. - expect(t1.collateralBalance.userTotalShare).to.equal( - t1.bobCollateralShare.add(t1.carolCollateralShare) - ); + expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare)); }); }); @@ -1214,14 +1046,9 @@ describe("Private Lending Pool", async () => { it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect( - pair.liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.be.revertedWith("PrivatePool: all are solvent"); + await expect(pair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( + "PrivatePool: all are solvent" + ); }); it("Should liquidate insolvent borrowers only", async () => { @@ -1236,16 +1063,10 @@ describe("Private Lending Pool", async () => { // That Alice she is also the lender makes no difference in the execution // path taken. - await expect( - pair - .connect(alice) - .liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.emit(pair, "LogSeizeCollateral"); + await expect(pair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.emit( + pair, + "LogSeizeCollateral" + ); const t1 = { totalDebt: await pair.totalDebt(), @@ -1259,10 +1080,7 @@ describe("Private Lending Pool", async () => { assetBalance: await pair.assetBalance(), collateralBalance: await pair.collateralBalance(), - aliceBentoGuineas: await bentoBox.balanceOf( - guineas.address, - alice.address - ), + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -1287,16 +1105,10 @@ describe("Private Lending Pool", async () => { .mul(9) .div(10); const minCollateralFeeShare = minCollateralLiquidatorShare.div(9); - const minCollateralLenderShare = bobMinCollateralTakenShare - .mul(10_000) - .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS); + const minCollateralLenderShare = bobMinCollateralTakenShare.mul(10_000).div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS); - expect(t1.collateralBalance.feesEarnedShare).to.be.gte( - minCollateralFeeShare - ); - expect(t1.collateralBalance.feesEarnedShare).to.be.lte( - minCollateralFeeShare.mul(10_001).div(10_000) - ); + expect(t1.collateralBalance.feesEarnedShare).to.be.gte(minCollateralFeeShare); + expect(t1.collateralBalance.feesEarnedShare).to.be.lte(minCollateralFeeShare.mul(10_001).div(10_000)); // Alice gets the bonus only, in kind, minus the protocol fee. The // contract gets the protocol fee over the bonus. @@ -1304,31 +1116,22 @@ describe("Private Lending Pool", async () => { expect(t1.aliceBentoGuineas).to.equal(t0.aliceBentoGuineas); // Alice the liquidator: - const aliceMinBentoWeth = t0.aliceBentoWeth.add( - minCollateralLiquidatorShare - ); + const aliceMinBentoWeth = t0.aliceBentoWeth.add(minCollateralLiquidatorShare); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte( - aliceMinBentoWeth.mul(10_001).div(10_000) - ); + expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); // Alice the lender: expect(t1.aliceCollateralShare).to.be.gte(minCollateralLenderShare); - expect(t1.aliceCollateralShare).to.be.lte( - minCollateralLenderShare.mul(10_001).div(10_000) - ); + expect(t1.aliceCollateralShare).to.be.lte(minCollateralLenderShare.mul(10_001).div(10_000)); // Asset reserves do not really change, except for the interest fee.. - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( - t1.blockTimestamp - t0.blockTimestamp - ) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = - t0.assetBalance.reservesShare.sub(maxInterestFee); + const minAssetReserves = t0.assetBalance.reservesShare.sub(maxInterestFee); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); // Carol was not insolvent, so that liquidation failed: @@ -1337,23 +1140,13 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal( - t0.totalDebt.base.sub(bobLiquidatePart) - ); + expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); - const bobMaxCollateralShare = t0.bobCollateralShare.sub( - bobMinCollateralTakenShare - ); + const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte( - bobMaxCollateralShare.mul(9999).div(10_000) - ); + expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); // Given that individual shares are as expected, this tests the total: - expect(t1.collateralBalance.userTotalShare).to.equal( - t1.bobCollateralShare - .add(t1.carolCollateralShare) - .add(t1.aliceCollateralShare) - ); + expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare).add(t1.aliceCollateralShare)); }); }); @@ -1383,24 +1176,12 @@ describe("Private Lending Pool", async () => { await coinPair.updateExchangeRate(); await dollar.connect(alice).approve(bentoBox.address, MaxUint256); - await bentoBox - .connect(alice) - .deposit( - dollar.address, - alice.address, - alice.address, - totalSupply.div(10), - 0 - ); + await bentoBox.connect(alice).deposit(dollar.address, alice.address, alice.address, totalSupply.div(10), 0); await coinPair.connect(alice).addAsset(false, totalSupply.div(10)); await coin.connect(bob).approve(bentoBox.address, MaxUint256); - await bentoBox - .connect(bob) - .deposit(coin.address, bob.address, bob.address, totalSupply, 0); - await coinPair - .connect(bob) - .addCollateral(bob.address, false, totalSupply); + await bentoBox.connect(bob).deposit(coin.address, bob.address, bob.address, totalSupply, 0); + await coinPair.connect(bob).addCollateral(bob.address, false, totalSupply); const assetBalance = await coinPair.assetBalance(); const collateralBalance = await coinPair.collateralBalance(); @@ -1414,25 +1195,15 @@ describe("Private Lending Pool", async () => { expect(bentoCoinTotals.elastic).to.equal(totalSupply); if (shouldPass) { - await expect(coinPair.connect(bob).borrow(bob.address, 1)) - .to.emit(coinPair, "LogBorrow") - .to.emit(bentoBox, "LogTransfer"); + await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.emit(coinPair, "LogBorrow").to.emit(bentoBox, "LogTransfer"); } else { - await expect( - coinPair.connect(bob).borrow(bob.address, 1) - ).to.be.revertedWith("BoringMath: Mul Overflow"); + await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.be.revertedWith("BoringMath: Mul Overflow"); } }; - it( - "Works with all SPELL as collateral (no strat losses)", - makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true) - ); + it("Works with all SPELL as collateral (no strat losses)", makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true)); // This fails with the old Kashi-style `isSolvent`: - it( - "No longer breaks at 393 billion (18-decimal) tokens", - makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true) - ); + it("No longer breaks at 393 billion (18-decimal) tokens", makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true)); }); }); diff --git a/test/PrivatePool.ts b/test/PrivatePool.ts index 67aeb42f..390fd508 100644 --- a/test/PrivatePool.ts +++ b/test/PrivatePool.ts @@ -3,7 +3,32 @@ import _ from "lodash"; const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; -const initTypes = { +const makeStructTypeString = (namedTypes) => `tuple(${_.map(namedTypes, (t, name) => `${t} ${name}`).join(", ")})`; + +const loanParams = { + valuation: "uint128", + expiration: "uint64", + openFeeBPS: "uint16", + annualInterestBPS: "uint16", + compoundInterestTerms: "uint8", +}; +const loanParamsArrayType = makeStructTypeString(loanParams) + "[]"; + +const typeDefaults = { + address: ZERO_ADDR, + "uint256[]": [], + "address[]": [], + bytes: "", + [loanParamsArrayType]: [], +}; + +const makeStructEncoder = (namedTypes) => { + // These rely on JS/TS iterating over the keys in the order they were defined: + const typeArray = [makeStructTypeString(namedTypes)]; + return (kvs) => ethers.utils.defaultAbiCoder.encode(typeArray, [_.mapValues(namedTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)]); +}; + +export const encodeInitData = makeStructEncoder({ collateral: "address", asset: "address", oracle: "address", @@ -16,17 +41,50 @@ const initTypes = { LIQUIDATION_MULTIPLIER_BPS: "uint16", BORROW_OPENING_FEE_BPS: "uint16", LIQUIDATION_SEIZE_COLLATERAL: "bool", +}); + +export const encodeInitDataNFT = makeStructEncoder({ + collateral: "address", + asset: "address", + lender: "address", + borrowers: "address[]", + tokenIds: "uint256[]", + loanParams: loanParamsArrayType, +}); + +export const encodeLoanParamsNFT = makeStructEncoder(loanParams); + +// Cook actions +export const Cook = { + ACTION_ADD_ASSET: 1, + ACTION_REPAY: 2, + ACTION_REMOVE_ASSET: 3, + ACTION_REMOVE_COLLATERAL: 4, + ACTION_BORROW: 5, + ACTION_GET_REPAY_SHARE: 6, + ACTION_GET_REPAY_PART: 7, + ACTION_ACCRUE: 8, + + // Functions that don't need accrue to be called + ACTION_ADD_COLLATERAL: 10, + ACTION_UPDATE_EXCHANGE_RATE: 11, + + // Function on BentoBox + ACTION_BENTO_DEPOSIT: 20, + ACTION_BENTO_WITHDRAW: 21, + ACTION_BENTO_TRANSFER: 22, + ACTION_BENTO_TRANSFER_MULTIPLE: 23, + ACTION_BENTO_SETAPPROVAL: 24, + + // Any external call (except to BentoBox) + ACTION_CALL: 30, + + USE_VALUE1: -1, + USE_VALUE2: -2, }; -const typeDefaults = { - address: ZERO_ADDR, - "address[]": [], - bytes: "", + +export const LoanStatus = { + INITIAL: 0, + COLLATERAL_DEPOSITED: 1, + TAKEN: 2, }; -// These rely on JS/TS iterating over the keys in the order they were defined: -const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); - -export const encodeInitData = (kvs) => - ethers.utils.defaultAbiCoder.encode( - [`tuple(${initTypeString})`], - [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] - ); diff --git a/test/PrivatePoolNFT.test.ts b/test/PrivatePoolNFT.test.ts new file mode 100644 index 00000000..9fa5fcc6 --- /dev/null +++ b/test/PrivatePoolNFT.test.ts @@ -0,0 +1,588 @@ +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; +import { expect } from "chai"; +import { BigNumberish, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; + +import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; +import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, PrivatePoolNFT } from "../typechain"; +import { describeSnapshot } from "./helpers"; +import { Cook, LoanStatus, encodeInitDataNFT, encodeLoanParamsNFT } from "./PrivatePool"; + +interface ILoanParams { + valuation: BigNumberish; + expiration: BigNumberish; + openFeeBPS: BigNumberish; + annualInterestBPS: BigNumberish; + compoundInterestTerms: BigNumberish; +} +interface PartialLoanParams { + valuation?: BigNumberish; + expiration?: BigNumberish; + openFeeBPS?: BigNumberish; + annualInterestBPS?: BigNumberish; + compoundInterestTerms?: BigNumberish; +} + +const { formatUnits } = ethers.utils; +const { MaxUint256, AddressZero, HashZero } = ethers.constants; + +const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; + +describe("Private Lending Pool", async () => { + let apes: ERC721Mock; + let guineas: ERC20Mock; + let bentoBox: BentoBoxMock; + let masterContract: PrivatePoolNFT; + let deployer: SignerWithAddress; + let alice: SignerWithAddress; + let bob: SignerWithAddress; + let carol: SignerWithAddress; + + // Named token IDs for testing.. + let apeIds: { + aliceOne: BigNumberish; + aliceTwo: BigNumberish; + bobOne: BigNumberish; + bobTwo: BigNumberish; + carolOne: BigNumberish; + carolTwo: BigNumberish; + }; + + const deployContract = async (name, ...args) => { + const contract = await ethers.getContractFactory(name).then((f) => f.deploy(...args)); + // Simpler way to "cast"? The above works as the result if we igore types.. + return ethers.getContractAt(name, contract.address); + }; + + const deployPool = async (initSettings = {}) => { + const fullSettings = { + collateral: apes.address, + asset: guineas.address, + lender: alice.address, + borrowers: [bob.address, carol.address], + ...initSettings, + }; + const deployTx = await bentoBox.deploy(masterContract.address, encodeInitDataNFT(fullSettings), false).then((tx) => tx.wait()); + for (const e of deployTx.events || []) { + if (e.eventSignature == "LogDeploy(address,bytes,address)") { + return ethers.getContractAt("PrivatePoolNFT", e.args?.cloneAddress); + } + } + throw new Error("Deploy event not found"); // (For the typechecker..) + }; + + const addToken = (pool, tokenId, params: PartialLoanParams) => + pool.connect(alice).updateLoanParams(tokenId, { + valuation: 0, + expiration: nextYear, + openFeeBPS: 1000, + annualInterestBPS: 2000, + compoundInterestTerms: 5, + ...params, + }); + + // Specific to the mock implementation.. + const mintApe = async (ownerAddress) => { + const id = await apes.totalSupply(); + await apes.mint(ownerAddress); + return id; + }; + + before(async () => { + const weth = await deployContract("WETH9Mock"); + bentoBox = await deployContract("BentoBoxMock", weth.address); + masterContract = await deployContract("PrivatePoolNFT", bentoBox.address); + await bentoBox.whitelistMasterContract(masterContract.address, true); + apes = await deployContract("ERC721Mock"); + guineas = await deployContract("ERC20Mock", getBigNumber(1_000_000)); + + const addresses = await getNamedAccounts(); + deployer = await ethers.getSigner(addresses.deployer); + alice = await ethers.getSigner(addresses.alice); + bob = await ethers.getSigner(addresses.bob); + carol = await ethers.getSigner(addresses.carol); + + const mc = masterContract.address; + const hz = HashZero; + for (const signer of [alice, bob, carol]) { + const addr = signer.address; + const bb = bentoBox.connect(signer); + await bb.setMasterContractApproval(addr, mc, true, 0, hz, hz); + + await guineas.transfer(addr, getBigNumber(10_000)); + await guineas.connect(signer).approve(bentoBox.address, MaxUint256); + await bb.deposit(guineas.address, addr, addr, getBigNumber(3000), 0); + } + await guineas.approve(bentoBox.address, MaxUint256); + await bentoBox.addProfit(guineas.address, getBigNumber(11000)); + + // Guineas: 9000 in, 11k profit => 9k shares is 20k guineas. + // ---- alice: + // Guineas: 7000.0 + // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) + + apeIds = { + aliceOne: await mintApe(alice.address), + aliceTwo: await mintApe(alice.address), + bobOne: await mintApe(bob.address), + bobTwo: await mintApe(bob.address), + carolOne: await mintApe(carol.address), + carolTwo: await mintApe(carol.address), + }; + }); + + describeSnapshot("Deployment", () => { + let pool: PrivatePoolNFT; + let tomorrow: Number; + + before(async () => { + tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; + + pool = await deployPool({ + tokenIds: [apeIds.bobOne, apeIds.carolTwo], + loanParams: [ + { + valuation: getBigNumber(10), + expiration: tomorrow, + openFeeBPS: 1000, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }, + { + valuation: getBigNumber(20), + expiration: tomorrow, + openFeeBPS: 800, + annualInterestBPS: 3000, + compoundInterestTerms: 5, + }, + ], + }); + }); + + it("Should deploy with expected parameters", async () => { + expect(await pool.lender()).to.equal(alice.address); + for (const { address } of [carol, bob]) { + expect(await pool.approvedBorrowers(address)).to.equal(true); + } + expect(await pool.approvedBorrowers(alice.address)).to.equal(false); + + const paramsOne = await pool.tokenLoanParams(apeIds.bobOne); + expect(paramsOne.valuation).to.equal(getBigNumber(10)); + expect(paramsOne.expiration).to.equal(tomorrow); + expect(paramsOne.openFeeBPS).to.equal(1000); + expect(paramsOne.annualInterestBPS).to.equal(2000); + expect(paramsOne.compoundInterestTerms).to.equal(4); + + const notProvided = await pool.tokenLoanParams(apeIds.aliceOne); + expect(notProvided.valuation).to.equal(0); + expect(notProvided.expiration).to.equal(0); + expect(notProvided.openFeeBPS).to.equal(0); + expect(notProvided.annualInterestBPS).to.equal(0); + expect(notProvided.compoundInterestTerms).to.equal(0); + }); + + it("Should reject bad settings", async () => { + await expect(deployPool({ collateral: AddressZero })).to.be.revertedWith("PrivatePool: bad pair"); + + await expect( + deployPool({ + tokenIds: [apeIds.bobOne], + loanParams: [ + { + valuation: getBigNumber(20), + expiration: tomorrow, + openFeeBPS: 10001, + annualInterestBPS: 3000, + compoundInterestTerms: 5, + }, + ], + }) + ).to.be.revertedWith("PrivatePool: open fee"); + }); + + it("Should refuse to initialize twice", async () => { + await expect(pool.init(encodeInitDataNFT({}))).to.be.revertedWith("PrivatePool: already initialized"); + }); + }); + + describeSnapshot("Add Asset", () => { + let pool: PrivatePoolNFT; + + before(async () => { + pool = await deployPool({}); + }); + + it("Should let the lender add assets", async () => { + const share = getBigNumber(450); + await expect(pool.connect(alice).addAsset(false, share)) + .to.emit(pool, "LogAddAsset") + .withArgs(alice.address, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pool.address, share); + + const assetBalance = await pool.assetBalance(); + expect(assetBalance.reservesShare).to.equal(share); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should let the lender add assets (skim)", async () => { + // This is not a reasonable transaction.. + const share = getBigNumber(450); + const [g, a, p] = [guineas, alice, pool].map((x) => x.address); + + await bentoBox.connect(alice).transfer(g, a, p, share); + await expect(pool.connect(alice).addAsset(true, share)).to.emit(pool, "LogAddAsset").withArgs(bentoBox.address, share); + + const assetBalance = await pool.assetBalance(); + expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should let the lender add assets (cook amount)", async () => { + // ( 10^9 ) ( 10^9 ) + const amount = 27_182_818_284_590_452_353n; // Does not divide 20 or 9 + + // (Shares : Amount) in Bento is (9 : 20) + // This is what the BentoBox gives us for our deposit; round down: + const share = (amount * 9n) / 20n; + + const [g, a, p] = [guineas, alice, pool].map((x) => x.address); + const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; + const datas = [ + encodeParameters(["address", "address", "uint256", "uint256"], [g, a, amount, 0]), + encodeParameters(["int256", "bool"], [share, false]), + ]; + const values = [0, 0]; + + // Make sure the existing Bento balance stays the same: + const initialBentoBalance = await bentoBox.balanceOf(g, a); + + await expect(pool.connect(alice).cook(actions, values, datas)) + .to.emit(bentoBox, "LogDeposit") + .withArgs(g, a, a, amount, share) + .to.emit(pool, "LogAddAsset") + .withArgs(a, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(g, a, p, share); + + expect(await bentoBox.balanceOf(g, a)).to.equal(initialBentoBalance); + + const assetBalance = await pool.assetBalance(); + expect(assetBalance.reservesShare).to.equal(share); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + + it("Should refuse to skim too much", async () => { + const share = getBigNumber(123); + const [g, a, p] = [guineas, alice, pool].map((x) => x.address); + + await bentoBox.connect(alice).transfer(g, a, p, share); + await expect(pool.connect(alice).addAsset(true, share.add(1))).to.be.revertedWith("PrivatePool: skim too much"); + }); + + it("Should let anyone add assets", async () => { + const share = getBigNumber(450); + await expect(pool.connect(bob).addAsset(false, share)) + .to.emit(pool, "LogAddAsset") + .withArgs(bob.address, share) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, bob.address, pool.address, share); + + const share2 = 27_182_818_284_590_452_353n; + await expect(pool.connect(carol).addAsset(false, share2)) + .to.emit(pool, "LogAddAsset") + .withArgs(carol.address, share2) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, carol.address, pool.address, share2); + + const assetBalance = await pool.assetBalance(); + expect(assetBalance.reservesShare).to.equal(share.add(share2)); + expect(assetBalance.feesEarnedShare).to.equal(0); + }); + }); + + describeSnapshot("Add Collateral", () => { + let pool: PrivatePoolNFT; + let tomorrow: Number; + + before(async () => { + tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; + + pool = await deployPool({ + tokenIds: [apeIds.aliceOne, apeIds.bobOne, apeIds.carolTwo], + loanParams: [ + { + valuation: getBigNumber(10), + expiration: tomorrow, + openFeeBPS: 1000, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }, + { + valuation: getBigNumber(10), + expiration: tomorrow, + openFeeBPS: 1000, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }, + { + valuation: getBigNumber(20), + expiration: tomorrow, + openFeeBPS: 800, + annualInterestBPS: 3000, + compoundInterestTerms: 5, + }, + ], + }); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pool.address, true); + } + }); + + it("Should accept approved tokens for approved borrowers", async () => { + const beforeStatus = await pool.tokenLoan(apeIds.bobOne); + expect(beforeStatus.borrower).to.equal(AddressZero); + expect(beforeStatus.startTime).to.equal(0); + expect(beforeStatus.status).to.equal(LoanStatus.INITIAL); + + await expect(pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false)) + .to.emit(pool, "LogAddCollateral") + .withArgs(bob.address, bob.address, apeIds.bobOne) + .to.emit(apes, "Transfer") + .withArgs(bob.address, pool.address, apeIds.bobOne); + + const afterStatus = await pool.tokenLoan(apeIds.bobOne); + expect(afterStatus.borrower).to.equal(bob.address); + expect(afterStatus.startTime).to.equal(0); + expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); + }); + + it("Should let anyone deposit for an approved borrower", async () => { + await apes.connect(bob).transferFrom(bob.address, deployer.address, apeIds.bobOne); + + await expect(pool.connect(deployer).addCollateral(apeIds.bobOne, bob.address, false)) + .to.emit(pool, "LogAddCollateral") + .withArgs(deployer.address, bob.address, apeIds.bobOne) + .to.emit(apes, "Transfer") + .withArgs(deployer.address, pool.address, apeIds.bobOne); + + const afterStatus = await pool.tokenLoan(apeIds.bobOne); + expect(afterStatus.borrower).to.equal(bob.address); + expect(afterStatus.startTime).to.equal(0); + expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); + }); + + it("Should refuse collateral for an unapproved borrower", async () => { + await expect(pool.connect(bob).addCollateral(apeIds.bobOne, alice.address, false)).to.be.revertedWith("PrivatePool: unapproved borrower"); + }); + + it("Should refuse unapproved tokens", async () => { + await expect(pool.connect(bob).addCollateral(apeIds.bobTwo, bob.address, false)).to.be.revertedWith("PrivatePool: loan unavailable"); + }); + + it("Should accept approved tokens (skim)", async () => { + await apes.connect(alice).transferFrom(alice.address, pool.address, apeIds.aliceOne); + + await expect(pool.connect(bob).addCollateral(apeIds.aliceOne, bob.address, true)) + .to.emit(pool, "LogAddCollateral") + .withArgs(pool.address, bob.address, apeIds.aliceOne); + + const afterStatus = await pool.tokenLoan(apeIds.aliceOne); + expect(afterStatus.borrower).to.equal(bob.address); + expect(afterStatus.startTime).to.equal(0); + expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); + }); + }); + + describeSnapshot("Borrow", async () => { + let pool: PrivatePoolNFT; + + before(async () => { + pool = await deployPool(); + for (const signer of [bob, carol]) { + await apes.connect(signer).setApprovalForAll(pool.address, true); + } + const share = getBigNumber(450); // 1000 guineas + await pool.connect(alice).addAsset(false, share); + + // Doubles as a test of setting the params the other way: + await addToken(pool, apeIds.bobOne, { + valuation: getBigNumber(1, 8), + expiration: nextYear, + annualInterestBPS: 10000, + }); + await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); + + await addToken(pool, apeIds.carolOne, { + valuation: getBigNumber(10), + expiration: nextYear, + annualInterestBPS: 3000, + openFeeBPS: 500, + compoundInterestTerms: 10, + }); + await pool.connect(carol).addCollateral(apeIds.carolOne, carol.address, false); + + // Allowed as collateral but not provided: + await addToken(pool, apeIds.carolTwo, { + valuation: getBigNumber(10), + }); + }); + + it("Should allow approved borrowers to borrow", async () => { + const [g, b, p] = [guineas, bob, pool].map((x) => x.address); + + const terms = await pool.tokenLoanParams(apeIds.bobOne); + const ts = await advanceNextTime(1); + + expect(terms.valuation).to.equal(getBigNumber(1, 8)); + const openFee = terms.valuation.mul(terms.openFeeBPS).div(10_000); + const receivedShare = terms.valuation.sub(openFee).mul(9).div(20); + const openFeeShare = openFee.mul(9).div(20); + const protocolFeeShare = openFeeShare.div(10); // Fixed + + const t0 = { + assetBalance: await pool.assetBalance(), + loanStatus: await pool.tokenLoan(apeIds.bobOne), + }; + + expect(t0.loanStatus.borrower).to.equal(bob.address); + expect(t0.loanStatus.startTime).to.equal(0); + expect(t0.loanStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); + + await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)) + .to.emit(pool, "LogBorrow") + .withArgs(b, b, apeIds.bobOne) + .to.emit(bentoBox, "LogTransfer") + .withArgs(g, p, b, receivedShare); + + const t1 = { + assetBalance: await pool.assetBalance(), + loanStatus: await pool.tokenLoan(apeIds.bobOne), + }; + + expect(t1.loanStatus.borrower).to.equal(bob.address); + expect(t1.loanStatus.startTime).to.equal(ts); + expect(t1.loanStatus.status).to.equal(LoanStatus.TAKEN); + + expect(t1.assetBalance.feesEarnedShare).to.equal(t0.assetBalance.feesEarnedShare.add(protocolFeeShare)); + expect(t1.assetBalance.reservesShare).to.equal(t0.assetBalance.reservesShare.sub(receivedShare).sub(protocolFeeShare)); + }); + + it("Should only lend to whoever supplied the collateral", async () => { + // Havinng supplied the collateral counts as being an "approved borrower". + // (This only matters if we actually add a way to change the whitelist). + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await expect(pool.connect(carol).borrow(apeIds.bobOne, carol.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); + + // Sending it to Bob won't help, he has to be the msg.sender: + await expect(pool.connect(carol).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); + }); + + it("Should not lend unless the collateral is deposited", async () => { + const terms = await pool.tokenLoanParams(apeIds.carolTwo); + await expect(pool.connect(carol).borrow(apeIds.carolTwo, carol.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); + }); + + it("Should not lend against the same collateral twice", async () => { + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.emit(pool, "LogBorrow"); + await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); + }); + + it("Should not lend if the loan is expired", async () => { + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); + await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: expired"); + }); + + it("Should allow loans at any time before expiration", async () => { + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); + await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.emit(pool, "LogBorrow"); + }); + }); + + describeSnapshot("Remove Collateral", () => { + let pool: PrivatePoolNFT; + + before(async () => { + pool = await deployPool(); + for (const signer of [bob, carol]) { + await apes.connect(signer).setApprovalForAll(pool.address, true); + } + const share = getBigNumber(450); // 1000 guineas + await pool.connect(alice).addAsset(false, share); + + await addToken(pool, apeIds.bobOne, { + valuation: getBigNumber(1, 8), + expiration: nextYear, + annualInterestBPS: 10000, + }); + await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); + + await addToken(pool, apeIds.carolOne, { + valuation: getBigNumber(1, 8), + expiration: nextYear, + annualInterestBPS: 10000, + }); + await pool.connect(carol).addCollateral(apeIds.carolOne, carol.address, false); + }); + + it("Should let depositors withdraw their unused collateral", async () => { + await expect(pool.connect(bob).removeCollateral(apeIds.bobOne, carol.address)) + .to.emit(pool, "LogRemoveCollateral") + .withArgs(bob.address, carol.address, apeIds.bobOne) + .to.emit(apes, "Transfer") + .withArgs(pool.address, carol.address, apeIds.bobOne); + + const loanStatus = await pool.tokenLoan(apeIds.bobOne); + expect(loanStatus.borrower).to.equal(AddressZero); + expect(loanStatus.startTime).to.equal(0); + expect(loanStatus.status).to.equal(LoanStatus.INITIAL); + }); + + it("Should not allow withdrawals if the collateral is in use", async () => { + // The only legitimate reason to take used collateral is a "liquidation" + // where the loan has expired. This is only available to the lender: + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + await expect(pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the lender"); + }); + + it("Should not give out someone else's unused collateral", async () => { + await expect(pool.connect(bob).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the borrower"); + }); + + it("Should let only the lender seize expired collateral", async () => { + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); + + await expect(pool.connect(bob).removeCollateral(apeIds.carolOne, bob.address)).to.be.revertedWith("PrivatePool: not the lender"); + + // You cannot "seize" your own collateral either: + await expect(pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the lender"); + + await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)) + .to.emit(pool, "LogRemoveCollateral") + .withArgs(carol.address, alice.address, apeIds.carolOne) + .to.emit(apes, "Transfer") + .withArgs(pool.address, alice.address, apeIds.carolOne); + }); + + it("Should only allow seizing expired collateral", async () => { + const terms = await pool.tokenLoanParams(apeIds.bobOne); + await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); + + await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)).to.be.revertedWith("PrivatePool: not expired"); + }); + + it("Should only allow seizing collateral used for a loan", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); + + await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)).to.be.revertedWith("PrivatePool: not the borrower"); + }); + }); +}); diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 00000000..b1bc679b --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,25 @@ +import { ethers } from "hardhat"; + +// Inner snapshots.The "inner" state is whatever `before` in `proc` sets up. +// Reverts to the "outer" state after: +export const describeSnapshot = (name, proc) => + describe(name, () => { + let outerSnapshotId; + before(async () => { + outerSnapshotId = await ethers.provider.send("evm_snapshot", []); + }); + + proc(); + + let snapshotId = null; + beforeEach(async () => { + if (snapshotId) { + await ethers.provider.send("evm_revert", [snapshotId]); + } + snapshotId = await ethers.provider.send("evm_snapshot", []); + }); + + after(async () => { + await ethers.provider.send("evm_revert", [outerSnapshotId]); + }); + }); diff --git a/tsconfig.json b/tsconfig.json index 056b81da..9a4e9b85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2018", + "target": "es2020", "module": "commonjs", "strict": true, "esModuleInterop": true, From 3c843d305b4a94e63fd3b9c5b5677c541fddd88b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 2 Feb 2022 04:43:42 +1100 Subject: [PATCH 029/107] Always allow liquidation of insolvent positions --- contracts/PrivatePool.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 607477aa..0d9ad057 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -153,7 +153,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { AccrueInfo memory _aI; _aI.INTEREST_PER_SECOND = settings.INTEREST_PER_SECOND; - _aI.EXPIRATION = settings.EXPIRATION; + _aI.EXPIRATION = settings.EXPIRATION == 0 ? uint64(-1) : settings.EXPIRATION; _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS; _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS; _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS; @@ -777,19 +777,20 @@ contract PrivatePool is BoringOwnable, IMasterContract { accrue(); AccrueInfo memory _accrueInfo = accrueInfo; - require(block.timestamp >= _accrueInfo.EXPIRATION, "PrivatePool: no liquidation yet"); uint256 allCollateralShare; uint256 allDebtAmount; uint256 allDebtPart; Rebase memory _totalDebt = totalDebt; Rebase memory bentoBoxTotals = bentoBox.totals(collateral); + for (uint256 i = 0; i < borrowers.length; i++) { + // TODO: Find a way to not have it here; only for stack reasons address borrower = borrowers[i]; // If we set an expiration at all, then by the above check it is // now past and every borrower can be liquidated at the current // price: - if ((_accrueInfo.EXPIRATION > 0) || !_isSolvent(borrower, _exchangeRate)) { + if (block.timestamp >= accrueInfo.EXPIRATION || !_isSolvent(borrower, _exchangeRate)) { uint256 debtPart; { uint256 availableDebtPart = borrowerDebtPart[borrower]; From 17a641d7bf094e63cba8c196a378b529e713f757 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 2 Feb 2022 04:43:47 +1100 Subject: [PATCH 030/107] Explicitly test PrivatePool contract size limit --- test/PrivatePool.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index 2beb5dae..e818401f 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -233,6 +233,14 @@ describe("Private Lending Pool", async () => { it("Should refuse to initialize twice", async () => { await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); }); + + it("Should not exceed the EIP-170 size limit", async () => { + // ..or just rely on Hardhat? + const code = await ethers.provider.send("eth_getCode", [masterContract.address]); + // Hex string, starting with "0x": + const byteCount = (code.length - 2) / 2; + expect(byteCount).to.be.lte(0x6000); + }); }); describeSnapshot("Add Asset", async () => { From 8b07fc55b1ecf6af84e541c5139211cec7308a82 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 2 Feb 2022 04:43:52 +1100 Subject: [PATCH 031/107] Always update exhchange rate in _isSolvent() --- contracts/PrivatePool.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 0d9ad057..6ec563b4 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -220,9 +220,9 @@ contract PrivatePool is BoringOwnable, IMasterContract { emit LogAccrue(extraAmount, feeAmount); } - /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`. - /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls. - function _isSolvent(address borrower, uint256 _exchangeRate) internal view returns (bool) { + function _isSolvent(address borrower) internal returns (bool) { + (, uint256 _exchangeRate) = updateExchangeRate(); + // accrue must have already been called! uint256 debtPart = borrowerDebtPart[borrower]; if (debtPart == 0) return true; @@ -340,7 +340,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body. modifier solvent() { _; - require(_isSolvent(msg.sender, exchangeRate), "PrivatePool: borrower insolvent"); + require(_isSolvent(msg.sender), "PrivatePool: borrower insolvent"); } /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset. @@ -757,7 +757,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { } if (status.needsSolvencyCheck) { - require(_isSolvent(msg.sender, exchangeRate), "PrivatePool: borrower insolvent"); + require(_isSolvent(msg.sender), "PrivatePool: borrower insolvent"); } } @@ -790,7 +790,7 @@ contract PrivatePool is BoringOwnable, IMasterContract { // If we set an expiration at all, then by the above check it is // now past and every borrower can be liquidated at the current // price: - if (block.timestamp >= accrueInfo.EXPIRATION || !_isSolvent(borrower, _exchangeRate)) { + if (block.timestamp >= accrueInfo.EXPIRATION || !_isSolvent(borrower)) { uint256 debtPart; { uint256 availableDebtPart = borrowerDebtPart[borrower]; From 824e179e15f4a17c2e1c8fe61d4b32c259d0ac6c Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 2 Feb 2022 04:43:54 +1100 Subject: [PATCH 032/107] Split in-kind liquidation bonus between liquidator and lender --- contracts/PrivatePool.sol | 29 +++++++++++++++++++---------- test/PrivatePool.test.ts | 17 ++++++++++++----- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/PrivatePool.sol b/contracts/PrivatePool.sol index 6ec563b4..74eb6423 100644 --- a/contracts/PrivatePool.sol +++ b/contracts/PrivatePool.sol @@ -846,29 +846,38 @@ contract PrivatePool is BoringOwnable, IMasterContract { totalDebt = _totalDebt; if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) { - // As with normal liquidations, the liquidator gets the excess, the - // protocol gets a cut of the excess, and the lender gets 100% of - // the value of the loan. + // Unlike normal liquidations, the liquidator and the lender share + // the excess. This compensates the lender for agreeing to payment + // in the collateral. The protocol gets a cut of the total excess. + // So the final distribution is + // - X% excess (configurable via LIQUIDATION_MULTIPLIER_BPS) + // - (10% of X) protocol fee + // - (45% of X) liquidator share of bonus + // - 100% + (45% of X) debt + lender share of bonus // allCollateralShare already includes the bonus, which in turn // includes the protocol fee. We round the bonus down to favor the - // lender, and the fee to favor the liquidator: + // lender, and the fee to favor the liquidator and lender: // Math: All collateral fits in 128 bits (BentoBox), so the // multiplications are safe: uint256 excessShare = (allCollateralShare * (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) / _accrueInfo.LIQUIDATION_MULTIPLIER_BPS; uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS; - uint256 lenderShare = allCollateralShare - excessShare; - // (Stack depth): liquidatorShare = excessShare - feeShare; - + uint256 liquidatorShare = (excessShare - feeShare) / 2; + // We would add more variables for clarity, but stack depth: { CollateralBalance memory _collateralBalance = collateralBalance; // No underflow: All amounts fit in the collateral Bento total - _collateralBalance.userTotalShare -= uint128(excessShare); + // The lender is also a "user", so only the fee and liquidator + // share leave the user total "account": + _collateralBalance.userTotalShare -= uint128(feeShare + liquidatorShare); _collateralBalance.feesEarnedShare += uint128(feeShare); collateralBalance = _collateralBalance; } - userCollateralShare[lender] += lenderShare; - bentoBox.transfer(collateral, address(this), to, excessShare - feeShare); + // The rest goes to the lender: + userCollateralShare[lender] += (allCollateralShare - feeShare - liquidatorShare); + // The liquidator gets the other half -- rounded in their favour, + // if applicable: + bentoBox.transfer(collateral, address(this), to, liquidatorShare); } else { // No underflow: allCollateralShare is the sum of quantities that // have successfully been taken out of user balances. diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index e818401f..0d76a7e1 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -1107,19 +1107,26 @@ describe("Private Lending Pool", async () => { .div(531); // These need not add up (rounding), but should be firm lower bounds: - const minCollateralLiquidatorShare = bobMinCollateralTakenShare + // Nine tenth of the excess is bonus, one tenth is fees: + const minCollateralBonusShare = bobMinCollateralTakenShare .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS - 10_000) .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) .mul(9) .div(10); - const minCollateralFeeShare = minCollateralLiquidatorShare.div(9); - const minCollateralLenderShare = bobMinCollateralTakenShare.mul(10_000).div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS); + const minCollateralFeeShare = minCollateralBonusShare.div(9); + // The liquidator and the lender each get half the bonus, and the lender + // gets the loan value as well: + const minCollateralLiquidatorShare = minCollateralBonusShare.div(2); + const minCollateralLenderShare = bobMinCollateralTakenShare + .mul(10_000) + .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .add(minCollateralLiquidatorShare); expect(t1.collateralBalance.feesEarnedShare).to.be.gte(minCollateralFeeShare); expect(t1.collateralBalance.feesEarnedShare).to.be.lte(minCollateralFeeShare.mul(10_001).div(10_000)); - // Alice gets the bonus only, in kind, minus the protocol fee. The - // contract gets the protocol fee over the bonus. + // Alice gets the liquidator part of the bonus only, in kind, minus the + // protocol fee. The contract gets the protocol fee over the bonus. // No repayment: expect(t1.aliceBentoGuineas).to.equal(t0.aliceBentoGuineas); From 16524b9364ac34713d45f4190dc99d4220c8cf00 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 2 Feb 2022 04:43:32 +1100 Subject: [PATCH 033/107] NFT pair revisions -- untested draft Major changes from "PrivatePoolNFT: - Do not restrict borrowers and lenders - Borrowers provide collateral and loan terms, lenders take or leave - Assets are transfered instantly on lend/repay; the contract holds no user balances - Paying off a loan ends it, rather than leaving the borrower with the option to take it out again. This is because loans are now initiated by the lender. --- contracts/{PrivatePoolNFT.sol => NFTPair.sol} | 393 ++++-------- contracts/interfaces/IERC721Receiver.sol | 22 - test/PrivatePool.ts | 1 - test/PrivatePoolNFT.test.ts | 588 ------------------ 4 files changed, 136 insertions(+), 868 deletions(-) rename contracts/{PrivatePoolNFT.sol => NFTPair.sol} (57%) delete mode 100644 contracts/interfaces/IERC721Receiver.sol delete mode 100644 test/PrivatePoolNFT.test.ts diff --git a/contracts/PrivatePoolNFT.sol b/contracts/NFTPair.sol similarity index 57% rename from contracts/PrivatePoolNFT.sol rename to contracts/NFTPair.sol index e805db22..fa7cb00d 100644 --- a/contracts/PrivatePoolNFT.sol +++ b/contracts/NFTPair.sol @@ -26,37 +26,42 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; -import "./interfaces/IERC721Receiver.sol"; -/// @title PrivatePoolNFT +/// @title NFTPair /// @dev This contract allows contract calls to any contract (except BentoBox) /// from arbitrary callers thus, don't trust calls from this contract in any circumstances. -contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { +contract NFTPair is BoringOwnable, IMasterContract { using BoringMath for uint256; using BoringMath128 for uint128; using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogAddCollateral(address indexed from, address indexed to, uint256 tokenId); - event LogAddAsset(address indexed from, uint256 share); - event LogRemoveCollateral(address indexed from, address indexed to, uint256 tokenId); - event LogRemoveAsset(address indexed to, uint256 share); - event LogBorrow(address indexed from, address indexed to, uint256 tokenId); - event LogRepay(address indexed from, uint256 tokenId); - event LogFeeTo(address indexed newFeeTo); - event LogWithdrawFees(address indexed feeTo, uint256 feeShare); + event LogRequestLoan( + address indexed borrower, + uint256 indexed tokenId, + uint128 valuation, + uint64 expiration, + uint16 annualInterestBPS, + uint8 compoundInterestTerms + ); event LogUpdateLoanParams( - uint256 tokenId, + uint256 indexed tokenId, uint128 valuation, uint64 expiration, - uint16 openFeeBPS, uint16 annualInterestBPS, uint8 compoundInterestTerms ); + // This automatically clears the associated loan, if any + event LogRemoveCollateral(uint256 indexed tokenId, address recipient); + // Details are in the loan request + event LogLend(address indexed lender, uint256 indexed tokenId); + event LogRepay(address indexed from, uint256 tokenId); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees(address indexed feeTo, uint256 feeShare); // Immutables (for MasterContract and all clones) IBentoBoxV1 public immutable bentoBox; - PrivatePoolNFT public immutable masterContract; + NFTPair public immutable masterContract; // MasterContract variables address public feeTo; @@ -66,42 +71,35 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { IERC721 public collateral; IERC20 public asset; - address public lender; - mapping(address => bool) public approvedBorrowers; - // A note on terminology: // "Shares" are BentoBox shares. - // The BentoBox balance is the sum of the below two. - struct AssetBalance { - uint128 reservesShare; - uint128 feesEarnedShare; - } - AssetBalance public assetBalance; + // Track assets we own. Used to allow skimming the excesss. + uint256 feesEarnedShare; // Per token settings. - // We might allow the lender to update these, but only to the benefit of - // (potential) borrowers struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 expiration; // Pay before this or get liquidated - uint16 openFeeBPS; // Fixed cost of taking out the loan uint16 annualInterestBPS; // Variable cost of taking out the loan uint8 compoundInterestTerms; // Might as well. Stay under 50. } mapping(uint256 => TokenLoanParams) public tokenLoanParams; uint8 private constant LOAN_INITIAL = 0; - uint8 private constant LOAN_COLLATERAL_DEPOSITED = 1; - uint8 private constant LOAN_TAKEN = 2; + uint8 private constant LOAN_REQUESTED = 1; + uint8 private constant LOAN_OUTSTANDING = 2; struct TokenLoan { address borrower; + address lender; uint64 startTime; uint8 status; } mapping(uint256 => TokenLoan) public tokenLoan; - uint256 private constant PROTOCOL_FEE_BPS = 1000; // Do not go over 100%.. + // Do not go over 100% on either of these.. + uint256 private constant PROTOCOL_FEE_BPS = 1000; + uint256 private constant OPEN_FEE_BPS = 100; uint256 private constant BPS = 10_000; uint256 private constant YEAR = 3600 * 24 * 365; @@ -112,254 +110,135 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { masterContract = this; } - struct InitSettings { - IERC721 collateral; - IERC20 asset; - address lender; - address[] borrowers; - uint256[] tokenIds; - TokenLoanParams[] loanParams; - } - /// @notice De facto constructor for clone contracts function init(bytes calldata data) public payable override { - require(address(collateral) == address(0), "PrivatePool: already initialized"); - - InitSettings memory settings = abi.decode(data, (InitSettings)); - require(address(settings.collateral) != address(0), "PrivatePool: bad pair"); - - collateral = settings.collateral; - asset = settings.asset; - lender = settings.lender; - - for (uint256 i = 0; i < settings.borrowers.length; i++) { - approvedBorrowers[settings.borrowers[i]] = true; - } - for (uint256 i = 0; i < settings.tokenIds.length; i++) { - _updateLoanParams(settings.tokenIds[i], settings.loanParams[i]); - } + require(address(collateral) == address(0), "NFTPair: already initialized"); + (collateral, asset) = abi.decode(data, (IERC721, IERC20)); + require(address(collateral) != address(0), "NFTPair: bad pair"); } - function setApprovedBorrowers(address borrower, bool approved) external onlyOwner { - approvedBorrowers[borrower] = approved; - } - - // Enforces that settings are valid - function _updateLoanParams(uint256 tokenId, TokenLoanParams memory params) internal { - require(params.openFeeBPS < BPS, "PrivatePool: open fee"); - tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams( - tokenId, - params.valuation, - params.expiration, - params.openFeeBPS, - params.annualInterestBPS, - params.compoundInterestTerms - ); - } - - // Enforces that changes only benefit the borrower, if any. - // Can be changed, but only in favour of the borrower. This includes giving - // them another shot. + // TODO: Somehow merge this with `updateLoanParams` function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { - require(msg.sender == lender, "PrivatePool: not the lender"); - uint8 loanStatus = tokenLoan[tokenId].status; - if (loanStatus == LOAN_TAKEN) { + TokenLoan memory loan = tokenLoan[tokenId]; + if (loan.status == LOAN_OUTSTANDING) { + // The lender can change terms so long as the changes are strictly + // the same or better for the borrower: + require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( params.expiration >= cur.expiration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.compoundInterestTerms <= cur.compoundInterestTerms, - "PrivatePool: worse params" + "NFTPair: worse params" ); + } else if (loan.status == LOAN_REQUESTED) { + // The borrower has already deposited the collateral and can + // change whatever they like + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else { + // The loan has not been taken out yet; the borrower needs to + // provide collateral. (TODO: Do that here?) + revert("NFTPair: no collateral"); } - _updateLoanParams(tokenId, params); + tokenLoanParams[tokenId] = params; + emit LogUpdateLoanParams(tokenId, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); } - /// @notice Adds `collateral` from msg.sender to the account `to`. - /// @param to The receiver of the tokens. - /// @param skim False if we need to transfer from msg.sender to the contract - /// @param tokenId The token to add as collateral - function addCollateral( + /// @notice Deposit an NFT as collateral and request a loan against it + /// @param tokenId ID of the NFT + /// @param to Address to receive the loan, or option to withdraw collateral + /// @param params Loan conditions on offer + /// @param skim True if the token has already been transfered + function requestLoan( uint256 tokenId, address to, + TokenLoanParams memory params, bool skim ) public { - require(approvedBorrowers[to], "PrivatePool: unapproved borrower"); - TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - require(loanParams.valuation > 0, "PrivatePool: loan unavailable"); - + // Edge case: valuation can be zero. That effectively gifts the NFT and + // is therefore a bad idea, but does not break the contract. + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: Loan exists"); if (skim) { - require(collateral.ownerOf(tokenId) == address(this), "PrivatePool: skim failed"); - require(tokenLoan[tokenId].status == LOAN_INITIAL, "PrivatePool: in use"); + require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); } else { - collateral.safeTransferFrom(msg.sender, address(this), tokenId); + collateral.transferFrom(msg.sender, address(this), tokenId); } TokenLoan memory loan; loan.borrower = to; - loan.status = LOAN_COLLATERAL_DEPOSITED; + loan.status = LOAN_REQUESTED; tokenLoan[tokenId] = loan; - emit LogAddCollateral(skim ? address(this) : msg.sender, to, tokenId); - } + tokenLoanParams[tokenId] = params; - // Equals to - // `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` - // which can be also obtained as - // `IERC721Receiver(0).onERC721Received.selector` - bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external override returns (bytes4) { - // We could check that this token can actually be used as collateral, - // but we leave that to the sender and save a little gas.. - return _ERC721_RECEIVED; + emit LogRequestLoan(to, tokenId, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); } /// @notice Removes `tokenId` as collateral and transfers it to `to`. - /// @param to The receiver of the token. + /// @notice This destroys the loan. /// @param tokenId The token + /// @param to The receiver of the token. function removeCollateral(uint256 tokenId, address to) public { TokenLoan memory loan = tokenLoan[tokenId]; - if (loan.status == LOAN_COLLATERAL_DEPOSITED) { + if (loan.status == LOAN_REQUESTED) { // We are withdrawing collateral that is not in use: - require(msg.sender == loan.borrower, "PrivatePool: not the borrower"); - } else { + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else if (loan.status == LOAN_OUTSTANDING) { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: - require(msg.sender == lender, "PrivatePool: not the lender"); - require(loan.status == LOAN_TAKEN, "PrivatePool: paid off"); - require(tokenLoanParams[tokenId].expiration <= block.timestamp, "PrivatePool: not expired"); + require(msg.sender == loan.lender, "NFTPair: not the lender"); + require(tokenLoanParams[tokenId].expiration <= block.timestamp, "NFTPair: not expired"); } - emit LogRemoveCollateral(loan.borrower, to, tokenId); + // If there somehow is collateral but no accompanying loan, then anyone + // can claim it by first requesting a loan with `skim` set to true, and + // then withdrawing. So we might as well allow it here.. delete tokenLoan[tokenId]; - collateral.safeTransferFrom(address(this), to, tokenId); - } - - /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. - /// @param toReservesShare Amount of shares to reserves. - /// @param toReservesAmount Token amount. Ignored if `toReservesShare` nonzero. - /// @param toFeesAmount Token fee amount. Ignored if `toReservesShare` nonzero. - function _receiveAsset( - bool skim, - uint256 toReservesShare, - // (There is no case where we pass along a fee in shares) - uint256 toReservesAmount, - uint256 toFeesAmount - ) internal { - IERC20 _asset = asset; - AssetBalance memory _assetBalance = assetBalance; - uint256 priorAssetTotalShare = _assetBalance.reservesShare + _assetBalance.feesEarnedShare; - Rebase memory bentoBoxTotals = bentoBox.totals(_asset); - - uint256 toFeesShare = 0; - if (toReservesShare == 0) { - toReservesShare = bentoBoxTotals.toBase(toReservesAmount, true); - if (toFeesAmount > 0) { - toFeesShare = bentoBoxTotals.toBase(toFeesAmount, false); - } - } - uint256 takenShare = toReservesShare.add(toFeesShare); - - // No overflow, cast safe: takenShare is bigger and fits in 128 bits if - // the transfer or skim succeeds - _assetBalance.reservesShare += uint128(toReservesShare); - _assetBalance.feesEarnedShare += uint128(toFeesShare); - assetBalance = _assetBalance; - - if (skim) { - require(takenShare <= bentoBox.balanceOf(_asset, address(this)).sub(priorAssetTotalShare), "PrivatePool: skim too much"); - } else { - bentoBox.transfer(_asset, msg.sender, address(this), takenShare); - } - } - - /// @notice Adds assets to the lending pair. - /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender. - /// False if tokens from msg.sender in `bentoBox` should be transferred. - /// @param share The amount of shares to add. - function addAsset(bool skim, uint256 share) public { - _receiveAsset(skim, share, 0, 0); - emit LogAddAsset(skim ? address(bentoBox) : msg.sender, share); - } - - /// @notice Removes an asset from msg.sender and transfers it to `to`. - /// @param to The address that receives the removed assets. - /// @param share The amount of shares to remove. - function removeAsset(address to, uint256 share) public { - require(msg.sender == lender, "PrivatePool: not the lender"); - // Cast safe: Bento transfer reverts unless stronger condition holds - assetBalance.reservesShare = assetBalance.reservesShare.sub(uint128(share)); - bentoBox.transfer(asset, address(this), to, share); - emit LogRemoveAsset(to, share); + collateral.transferFrom(address(this), to, tokenId); + emit LogRemoveCollateral(tokenId, to); } - /// Returns the Bento shares received. Passes the entire loan parameters as - /// a safeguard against these being "frontrun". - function borrow( + /// @notice Lends with the parameters specified by the borrower. + /// @param tokenId ID of the token that will function as collateral + /// @param accepted Loan parameters as the lender saw them, for security + /// @param skim True if the assets have been transfered to the cauldron + function lend( uint256 tokenId, - address to, - TokenLoanParams memory offered - ) public returns (uint256 share, uint256 amount) { - require(approvedBorrowers[msg.sender], "PrivatePool: unapproved borrower"); - + TokenLoanParams memory accepted, + bool skim + ) public { TokenLoan memory loan = tokenLoan[tokenId]; - // If you managed to add the collateral, then you are approved. (Even - // if we add a method to update the borrower whitelist later..) - require(loan.status == LOAN_COLLATERAL_DEPOSITED && loan.borrower == msg.sender, "PrivatePool: no collateral"); + require(loan.status == LOAN_REQUESTED, "NFTPair: Not available"); TokenLoanParams memory params = tokenLoanParams[tokenId]; - require(params.expiration > block.timestamp, "PrivatePool: expired"); // Valuation has to be an exact match, everything else must be at least - // as cheap as promised: + // as good for the lender as `accepted`. require( - params.valuation == offered.valuation && - params.expiration >= offered.expiration && - params.openFeeBPS <= offered.openFeeBPS && - params.annualInterestBPS <= offered.annualInterestBPS && - params.compoundInterestTerms <= offered.compoundInterestTerms, - "PrivatePool: bad params" + params.valuation == accepted.valuation && + params.expiration <= accepted.expiration && + params.annualInterestBPS >= accepted.annualInterestBPS && + params.compoundInterestTerms >= accepted.compoundInterestTerms, + "NFTPair: bad params" ); - IERC20 _asset = asset; - Rebase memory bentoBoxTotals = bentoBox.totals(_asset); - - uint256 protocolFeeShare; - { - // No overflow: max 128 + 16 bits - uint256 openFeeAmount = (uint256(params.valuation) * params.openFeeBPS) / BPS; - // No overflow: max 144 + 16 bits - uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS; - // No underflow: openFeeBPS < BPS is enforced. - amount = params.valuation - openFeeAmount; - protocolFeeShare = bentoBoxTotals.toBase(protocolFeeAmount, false); - share = bentoBoxTotals.toBase(amount, false); - } + uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); + // No overflow: at most 128 + 16 bits (fits in BentoBox) + uint256 protocolFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; - { - AssetBalance memory _assetBalance = assetBalance; - // No overflow on the add: protocolFeeShare < share < Bento total, - // or the transfer reverts. The transfer is independent of the - // results of these calculations: `share` is not modified. - // Theoretically the fee could just make it overflow 128 bits. - // Underflow check is core business logic: - _assetBalance.reservesShare = _assetBalance.reservesShare.sub((share + protocolFeeShare).to128()); - // Cast is safe: `share` fits. Also, the checked cast above - // succeeded. No overflow: protocolFeeShare < reservesShare, and - // both balances together fit in the Bento share balance, - _assetBalance.feesEarnedShare += uint128(protocolFeeShare); - assetBalance = _assetBalance; + if (skim) { + require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + } else { + bentoBox.transfer(asset, msg.sender, address(this), totalShare); } + // No underflow: follows from OPEN_FEE_BPS <= BPS + uint256 borrowerShare = totalShare - protocolFeeShare; + bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); + // No overflow: addends (and result) must fit in BentoBox + feesEarnedShare += protocolFeeShare; - loan.status = LOAN_TAKEN; + loan.status = LOAN_OUTSTANDING; loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; - bentoBox.transfer(_asset, address(this), to, share); - emit LogBorrow(msg.sender, to, tokenId); + + emit LogLend(msg.sender, tokenId); } /// Approximates continuous compounding. Uses Horner's method to evaluate @@ -444,16 +323,17 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { for (uint256 k = 2; k <= n; k++) { term *= x; // Safe: See above. denom *= k; // Fits up to k = 50; no problem after - term /= denom; // Safe until n = 251 + term /= denom; // Safe until k = 251 interest = interest.add(term); // <- Only overflow check we need } } function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_TAKEN, "PrivatePool: no loan"); + require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); + require(loan.borrower == msg.sender, "NFTPair: not the borrower"); TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - require(loanParams.expiration > block.timestamp, "PrivatePool: loan expired"); + require(loanParams.expiration > block.timestamp, "NFTPair: loan expired"); uint256 principal = loanParams.valuation; @@ -464,24 +344,35 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { loanParams.annualInterestBPS, loanParams.compoundInterestTerms ).to128(); + uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; // No overflow (both lines): to128() would have reverted amount = principal + interest; - uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; + uint256 totalShare = bentoBox.toShare(asset, amount, false); + uint256 feeShare = bentoBox.toShare(asset, fee, false); + + address from; + if (skim) { + require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + from = address(this); + // No overflow: result fits in BentoBox + } else { + bentoBox.transfer(asset, msg.sender, address(this), feeShare); + from = msg.sender; + } // No underflow: PROTOCOL_FEE_BPS < BPS by construction. - _receiveAsset(skim, 0, amount - fee, fee); - loan.status = LOAN_COLLATERAL_DEPOSITED; - tokenLoan[tokenId] = loan; - emit LogRepay(skim ? address(bentoBox) : msg.sender, tokenId); + feesEarnedShare += feeShare; + bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); + collateral.transferFrom(address(this), msg.sender, tokenId); + delete tokenLoan[tokenId]; + + emit LogRepay(from, tokenId); } - uint8 internal constant ACTION_ADD_ASSET = 1; uint8 internal constant ACTION_REPAY = 2; - uint8 internal constant ACTION_REMOVE_ASSET = 3; uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; - uint8 internal constant ACTION_BORROW = 5; - uint8 internal constant ACTION_ADD_COLLATERAL = 10; + uint8 internal constant ACTION_REQUEST_LOAN = 12; // Function on BentoBox uint8 internal constant ACTION_BENTO_DEPOSIT = 20; @@ -550,10 +441,10 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { callData = abi.encodePacked(callData, value1, value2); } - require(callee != address(bentoBox) && callee != address(this), "PrivatePool: can't call"); + require(callee != address(bentoBox) && callee != address(this), "NFTPair: can't call"); (bool success, bytes memory returnData) = callee.call{value: value}(callData); - require(success, "PrivatePool: call failed"); + require(success, "NFTPair: call failed"); return (returnData, returnValues); } @@ -571,24 +462,12 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { ) external payable returns (uint256 value1, uint256 value2) { for (uint256 i = 0; i < actions.length; i++) { uint8 action = actions[i]; - if (action == ACTION_ADD_COLLATERAL) { - (uint256 tokenId, address to, bool skim) = abi.decode(datas[i], (uint256, address, bool)); - addCollateral(tokenId, to, skim); - } else if (action == ACTION_ADD_ASSET) { - (int256 share, bool skim) = abi.decode(datas[i], (int256, bool)); - addAsset(skim, _num(share, value1, value2)); - } else if (action == ACTION_REPAY) { + if (action == ACTION_REPAY) { (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); repay(tokenId, skim); - } else if (action == ACTION_REMOVE_ASSET) { - (int256 share, address to) = abi.decode(datas[i], (int256, address)); - removeAsset(to, _num(share, value1, value2)); } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); - } else if (action == ACTION_BORROW) { - (uint256 tokenId, address to, TokenLoanParams memory offered) = abi.decode(datas[i], (uint256, address, TokenLoanParams)); - (value1, value2) = borrow(tokenId, to, offered); } else if (action == ACTION_BENTO_SETAPPROVAL) { (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], @@ -621,13 +500,13 @@ contract PrivatePoolNFT is BoringOwnable, IMasterContract, IERC721Receiver { function withdrawFees() public { address to = masterContract.feeTo(); - uint256 assetShare = assetBalance.feesEarnedShare; - if (assetShare > 0) { - bentoBox.transfer(asset, address(this), to, assetShare); - assetBalance.feesEarnedShare = 0; + uint256 _share = feesEarnedShare; + if (_share > 0) { + bentoBox.transfer(asset, address(this), to, _share); + feesEarnedShare = 0; } - emit LogWithdrawFees(to, assetShare); + emit LogWithdrawFees(to, _share); } /// @notice Sets the beneficiary of fees accrued in liquidations. diff --git a/contracts/interfaces/IERC721Receiver.sol b/contracts/interfaces/IERC721Receiver.sol deleted file mode 100644 index c64a0521..00000000 --- a/contracts/interfaces/IERC721Receiver.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -// Taken from OpenZeppelin contracts v3 - -pragma solidity >=0.6.0 <0.8.0; - -/** - * @title ERC721 token receiver interface - * @dev Interface for any contract that wants to support safeTransfers - * from ERC721 asset contracts. - */ -interface IERC721Receiver { - /** - * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom} - * by `operator` from `from`, this function is called. - * - * It must return its Solidity selector to confirm the token transfer. - * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. - * - * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`. - */ - function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); -} diff --git a/test/PrivatePool.ts b/test/PrivatePool.ts index 390fd508..275e1647 100644 --- a/test/PrivatePool.ts +++ b/test/PrivatePool.ts @@ -47,7 +47,6 @@ export const encodeInitDataNFT = makeStructEncoder({ collateral: "address", asset: "address", lender: "address", - borrowers: "address[]", tokenIds: "uint256[]", loanParams: loanParamsArrayType, }); diff --git a/test/PrivatePoolNFT.test.ts b/test/PrivatePoolNFT.test.ts deleted file mode 100644 index 9fa5fcc6..00000000 --- a/test/PrivatePoolNFT.test.ts +++ /dev/null @@ -1,588 +0,0 @@ -import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; -import { expect } from "chai"; -import { BigNumberish, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; - -import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; -import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, PrivatePoolNFT } from "../typechain"; -import { describeSnapshot } from "./helpers"; -import { Cook, LoanStatus, encodeInitDataNFT, encodeLoanParamsNFT } from "./PrivatePool"; - -interface ILoanParams { - valuation: BigNumberish; - expiration: BigNumberish; - openFeeBPS: BigNumberish; - annualInterestBPS: BigNumberish; - compoundInterestTerms: BigNumberish; -} -interface PartialLoanParams { - valuation?: BigNumberish; - expiration?: BigNumberish; - openFeeBPS?: BigNumberish; - annualInterestBPS?: BigNumberish; - compoundInterestTerms?: BigNumberish; -} - -const { formatUnits } = ethers.utils; -const { MaxUint256, AddressZero, HashZero } = ethers.constants; - -const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; - -describe("Private Lending Pool", async () => { - let apes: ERC721Mock; - let guineas: ERC20Mock; - let bentoBox: BentoBoxMock; - let masterContract: PrivatePoolNFT; - let deployer: SignerWithAddress; - let alice: SignerWithAddress; - let bob: SignerWithAddress; - let carol: SignerWithAddress; - - // Named token IDs for testing.. - let apeIds: { - aliceOne: BigNumberish; - aliceTwo: BigNumberish; - bobOne: BigNumberish; - bobTwo: BigNumberish; - carolOne: BigNumberish; - carolTwo: BigNumberish; - }; - - const deployContract = async (name, ...args) => { - const contract = await ethers.getContractFactory(name).then((f) => f.deploy(...args)); - // Simpler way to "cast"? The above works as the result if we igore types.. - return ethers.getContractAt(name, contract.address); - }; - - const deployPool = async (initSettings = {}) => { - const fullSettings = { - collateral: apes.address, - asset: guineas.address, - lender: alice.address, - borrowers: [bob.address, carol.address], - ...initSettings, - }; - const deployTx = await bentoBox.deploy(masterContract.address, encodeInitDataNFT(fullSettings), false).then((tx) => tx.wait()); - for (const e of deployTx.events || []) { - if (e.eventSignature == "LogDeploy(address,bytes,address)") { - return ethers.getContractAt("PrivatePoolNFT", e.args?.cloneAddress); - } - } - throw new Error("Deploy event not found"); // (For the typechecker..) - }; - - const addToken = (pool, tokenId, params: PartialLoanParams) => - pool.connect(alice).updateLoanParams(tokenId, { - valuation: 0, - expiration: nextYear, - openFeeBPS: 1000, - annualInterestBPS: 2000, - compoundInterestTerms: 5, - ...params, - }); - - // Specific to the mock implementation.. - const mintApe = async (ownerAddress) => { - const id = await apes.totalSupply(); - await apes.mint(ownerAddress); - return id; - }; - - before(async () => { - const weth = await deployContract("WETH9Mock"); - bentoBox = await deployContract("BentoBoxMock", weth.address); - masterContract = await deployContract("PrivatePoolNFT", bentoBox.address); - await bentoBox.whitelistMasterContract(masterContract.address, true); - apes = await deployContract("ERC721Mock"); - guineas = await deployContract("ERC20Mock", getBigNumber(1_000_000)); - - const addresses = await getNamedAccounts(); - deployer = await ethers.getSigner(addresses.deployer); - alice = await ethers.getSigner(addresses.alice); - bob = await ethers.getSigner(addresses.bob); - carol = await ethers.getSigner(addresses.carol); - - const mc = masterContract.address; - const hz = HashZero; - for (const signer of [alice, bob, carol]) { - const addr = signer.address; - const bb = bentoBox.connect(signer); - await bb.setMasterContractApproval(addr, mc, true, 0, hz, hz); - - await guineas.transfer(addr, getBigNumber(10_000)); - await guineas.connect(signer).approve(bentoBox.address, MaxUint256); - await bb.deposit(guineas.address, addr, addr, getBigNumber(3000), 0); - } - await guineas.approve(bentoBox.address, MaxUint256); - await bentoBox.addProfit(guineas.address, getBigNumber(11000)); - - // Guineas: 9000 in, 11k profit => 9k shares is 20k guineas. - // ---- alice: - // Guineas: 7000.0 - // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) - - apeIds = { - aliceOne: await mintApe(alice.address), - aliceTwo: await mintApe(alice.address), - bobOne: await mintApe(bob.address), - bobTwo: await mintApe(bob.address), - carolOne: await mintApe(carol.address), - carolTwo: await mintApe(carol.address), - }; - }); - - describeSnapshot("Deployment", () => { - let pool: PrivatePoolNFT; - let tomorrow: Number; - - before(async () => { - tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; - - pool = await deployPool({ - tokenIds: [apeIds.bobOne, apeIds.carolTwo], - loanParams: [ - { - valuation: getBigNumber(10), - expiration: tomorrow, - openFeeBPS: 1000, - annualInterestBPS: 2000, - compoundInterestTerms: 4, - }, - { - valuation: getBigNumber(20), - expiration: tomorrow, - openFeeBPS: 800, - annualInterestBPS: 3000, - compoundInterestTerms: 5, - }, - ], - }); - }); - - it("Should deploy with expected parameters", async () => { - expect(await pool.lender()).to.equal(alice.address); - for (const { address } of [carol, bob]) { - expect(await pool.approvedBorrowers(address)).to.equal(true); - } - expect(await pool.approvedBorrowers(alice.address)).to.equal(false); - - const paramsOne = await pool.tokenLoanParams(apeIds.bobOne); - expect(paramsOne.valuation).to.equal(getBigNumber(10)); - expect(paramsOne.expiration).to.equal(tomorrow); - expect(paramsOne.openFeeBPS).to.equal(1000); - expect(paramsOne.annualInterestBPS).to.equal(2000); - expect(paramsOne.compoundInterestTerms).to.equal(4); - - const notProvided = await pool.tokenLoanParams(apeIds.aliceOne); - expect(notProvided.valuation).to.equal(0); - expect(notProvided.expiration).to.equal(0); - expect(notProvided.openFeeBPS).to.equal(0); - expect(notProvided.annualInterestBPS).to.equal(0); - expect(notProvided.compoundInterestTerms).to.equal(0); - }); - - it("Should reject bad settings", async () => { - await expect(deployPool({ collateral: AddressZero })).to.be.revertedWith("PrivatePool: bad pair"); - - await expect( - deployPool({ - tokenIds: [apeIds.bobOne], - loanParams: [ - { - valuation: getBigNumber(20), - expiration: tomorrow, - openFeeBPS: 10001, - annualInterestBPS: 3000, - compoundInterestTerms: 5, - }, - ], - }) - ).to.be.revertedWith("PrivatePool: open fee"); - }); - - it("Should refuse to initialize twice", async () => { - await expect(pool.init(encodeInitDataNFT({}))).to.be.revertedWith("PrivatePool: already initialized"); - }); - }); - - describeSnapshot("Add Asset", () => { - let pool: PrivatePoolNFT; - - before(async () => { - pool = await deployPool({}); - }); - - it("Should let the lender add assets", async () => { - const share = getBigNumber(450); - await expect(pool.connect(alice).addAsset(false, share)) - .to.emit(pool, "LogAddAsset") - .withArgs(alice.address, share) - .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, alice.address, pool.address, share); - - const assetBalance = await pool.assetBalance(); - expect(assetBalance.reservesShare).to.equal(share); - expect(assetBalance.feesEarnedShare).to.equal(0); - }); - - it("Should let the lender add assets (skim)", async () => { - // This is not a reasonable transaction.. - const share = getBigNumber(450); - const [g, a, p] = [guineas, alice, pool].map((x) => x.address); - - await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(pool.connect(alice).addAsset(true, share)).to.emit(pool, "LogAddAsset").withArgs(bentoBox.address, share); - - const assetBalance = await pool.assetBalance(); - expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); - expect(assetBalance.feesEarnedShare).to.equal(0); - }); - - it("Should let the lender add assets (cook amount)", async () => { - // ( 10^9 ) ( 10^9 ) - const amount = 27_182_818_284_590_452_353n; // Does not divide 20 or 9 - - // (Shares : Amount) in Bento is (9 : 20) - // This is what the BentoBox gives us for our deposit; round down: - const share = (amount * 9n) / 20n; - - const [g, a, p] = [guineas, alice, pool].map((x) => x.address); - const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; - const datas = [ - encodeParameters(["address", "address", "uint256", "uint256"], [g, a, amount, 0]), - encodeParameters(["int256", "bool"], [share, false]), - ]; - const values = [0, 0]; - - // Make sure the existing Bento balance stays the same: - const initialBentoBalance = await bentoBox.balanceOf(g, a); - - await expect(pool.connect(alice).cook(actions, values, datas)) - .to.emit(bentoBox, "LogDeposit") - .withArgs(g, a, a, amount, share) - .to.emit(pool, "LogAddAsset") - .withArgs(a, share) - .to.emit(bentoBox, "LogTransfer") - .withArgs(g, a, p, share); - - expect(await bentoBox.balanceOf(g, a)).to.equal(initialBentoBalance); - - const assetBalance = await pool.assetBalance(); - expect(assetBalance.reservesShare).to.equal(share); - expect(assetBalance.feesEarnedShare).to.equal(0); - }); - - it("Should refuse to skim too much", async () => { - const share = getBigNumber(123); - const [g, a, p] = [guineas, alice, pool].map((x) => x.address); - - await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(pool.connect(alice).addAsset(true, share.add(1))).to.be.revertedWith("PrivatePool: skim too much"); - }); - - it("Should let anyone add assets", async () => { - const share = getBigNumber(450); - await expect(pool.connect(bob).addAsset(false, share)) - .to.emit(pool, "LogAddAsset") - .withArgs(bob.address, share) - .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, bob.address, pool.address, share); - - const share2 = 27_182_818_284_590_452_353n; - await expect(pool.connect(carol).addAsset(false, share2)) - .to.emit(pool, "LogAddAsset") - .withArgs(carol.address, share2) - .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, carol.address, pool.address, share2); - - const assetBalance = await pool.assetBalance(); - expect(assetBalance.reservesShare).to.equal(share.add(share2)); - expect(assetBalance.feesEarnedShare).to.equal(0); - }); - }); - - describeSnapshot("Add Collateral", () => { - let pool: PrivatePoolNFT; - let tomorrow: Number; - - before(async () => { - tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; - - pool = await deployPool({ - tokenIds: [apeIds.aliceOne, apeIds.bobOne, apeIds.carolTwo], - loanParams: [ - { - valuation: getBigNumber(10), - expiration: tomorrow, - openFeeBPS: 1000, - annualInterestBPS: 2000, - compoundInterestTerms: 4, - }, - { - valuation: getBigNumber(10), - expiration: tomorrow, - openFeeBPS: 1000, - annualInterestBPS: 2000, - compoundInterestTerms: 4, - }, - { - valuation: getBigNumber(20), - expiration: tomorrow, - openFeeBPS: 800, - annualInterestBPS: 3000, - compoundInterestTerms: 5, - }, - ], - }); - - for (const signer of [deployer, alice, bob, carol]) { - await apes.connect(signer).setApprovalForAll(pool.address, true); - } - }); - - it("Should accept approved tokens for approved borrowers", async () => { - const beforeStatus = await pool.tokenLoan(apeIds.bobOne); - expect(beforeStatus.borrower).to.equal(AddressZero); - expect(beforeStatus.startTime).to.equal(0); - expect(beforeStatus.status).to.equal(LoanStatus.INITIAL); - - await expect(pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false)) - .to.emit(pool, "LogAddCollateral") - .withArgs(bob.address, bob.address, apeIds.bobOne) - .to.emit(apes, "Transfer") - .withArgs(bob.address, pool.address, apeIds.bobOne); - - const afterStatus = await pool.tokenLoan(apeIds.bobOne); - expect(afterStatus.borrower).to.equal(bob.address); - expect(afterStatus.startTime).to.equal(0); - expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); - }); - - it("Should let anyone deposit for an approved borrower", async () => { - await apes.connect(bob).transferFrom(bob.address, deployer.address, apeIds.bobOne); - - await expect(pool.connect(deployer).addCollateral(apeIds.bobOne, bob.address, false)) - .to.emit(pool, "LogAddCollateral") - .withArgs(deployer.address, bob.address, apeIds.bobOne) - .to.emit(apes, "Transfer") - .withArgs(deployer.address, pool.address, apeIds.bobOne); - - const afterStatus = await pool.tokenLoan(apeIds.bobOne); - expect(afterStatus.borrower).to.equal(bob.address); - expect(afterStatus.startTime).to.equal(0); - expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); - }); - - it("Should refuse collateral for an unapproved borrower", async () => { - await expect(pool.connect(bob).addCollateral(apeIds.bobOne, alice.address, false)).to.be.revertedWith("PrivatePool: unapproved borrower"); - }); - - it("Should refuse unapproved tokens", async () => { - await expect(pool.connect(bob).addCollateral(apeIds.bobTwo, bob.address, false)).to.be.revertedWith("PrivatePool: loan unavailable"); - }); - - it("Should accept approved tokens (skim)", async () => { - await apes.connect(alice).transferFrom(alice.address, pool.address, apeIds.aliceOne); - - await expect(pool.connect(bob).addCollateral(apeIds.aliceOne, bob.address, true)) - .to.emit(pool, "LogAddCollateral") - .withArgs(pool.address, bob.address, apeIds.aliceOne); - - const afterStatus = await pool.tokenLoan(apeIds.aliceOne); - expect(afterStatus.borrower).to.equal(bob.address); - expect(afterStatus.startTime).to.equal(0); - expect(afterStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); - }); - }); - - describeSnapshot("Borrow", async () => { - let pool: PrivatePoolNFT; - - before(async () => { - pool = await deployPool(); - for (const signer of [bob, carol]) { - await apes.connect(signer).setApprovalForAll(pool.address, true); - } - const share = getBigNumber(450); // 1000 guineas - await pool.connect(alice).addAsset(false, share); - - // Doubles as a test of setting the params the other way: - await addToken(pool, apeIds.bobOne, { - valuation: getBigNumber(1, 8), - expiration: nextYear, - annualInterestBPS: 10000, - }); - await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); - - await addToken(pool, apeIds.carolOne, { - valuation: getBigNumber(10), - expiration: nextYear, - annualInterestBPS: 3000, - openFeeBPS: 500, - compoundInterestTerms: 10, - }); - await pool.connect(carol).addCollateral(apeIds.carolOne, carol.address, false); - - // Allowed as collateral but not provided: - await addToken(pool, apeIds.carolTwo, { - valuation: getBigNumber(10), - }); - }); - - it("Should allow approved borrowers to borrow", async () => { - const [g, b, p] = [guineas, bob, pool].map((x) => x.address); - - const terms = await pool.tokenLoanParams(apeIds.bobOne); - const ts = await advanceNextTime(1); - - expect(terms.valuation).to.equal(getBigNumber(1, 8)); - const openFee = terms.valuation.mul(terms.openFeeBPS).div(10_000); - const receivedShare = terms.valuation.sub(openFee).mul(9).div(20); - const openFeeShare = openFee.mul(9).div(20); - const protocolFeeShare = openFeeShare.div(10); // Fixed - - const t0 = { - assetBalance: await pool.assetBalance(), - loanStatus: await pool.tokenLoan(apeIds.bobOne), - }; - - expect(t0.loanStatus.borrower).to.equal(bob.address); - expect(t0.loanStatus.startTime).to.equal(0); - expect(t0.loanStatus.status).to.equal(LoanStatus.COLLATERAL_DEPOSITED); - - await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)) - .to.emit(pool, "LogBorrow") - .withArgs(b, b, apeIds.bobOne) - .to.emit(bentoBox, "LogTransfer") - .withArgs(g, p, b, receivedShare); - - const t1 = { - assetBalance: await pool.assetBalance(), - loanStatus: await pool.tokenLoan(apeIds.bobOne), - }; - - expect(t1.loanStatus.borrower).to.equal(bob.address); - expect(t1.loanStatus.startTime).to.equal(ts); - expect(t1.loanStatus.status).to.equal(LoanStatus.TAKEN); - - expect(t1.assetBalance.feesEarnedShare).to.equal(t0.assetBalance.feesEarnedShare.add(protocolFeeShare)); - expect(t1.assetBalance.reservesShare).to.equal(t0.assetBalance.reservesShare.sub(receivedShare).sub(protocolFeeShare)); - }); - - it("Should only lend to whoever supplied the collateral", async () => { - // Havinng supplied the collateral counts as being an "approved borrower". - // (This only matters if we actually add a way to change the whitelist). - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await expect(pool.connect(carol).borrow(apeIds.bobOne, carol.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); - - // Sending it to Bob won't help, he has to be the msg.sender: - await expect(pool.connect(carol).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); - }); - - it("Should not lend unless the collateral is deposited", async () => { - const terms = await pool.tokenLoanParams(apeIds.carolTwo); - await expect(pool.connect(carol).borrow(apeIds.carolTwo, carol.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); - }); - - it("Should not lend against the same collateral twice", async () => { - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.emit(pool, "LogBorrow"); - await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: no collateral"); - }); - - it("Should not lend if the loan is expired", async () => { - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); - await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.be.revertedWith("PrivatePool: expired"); - }); - - it("Should allow loans at any time before expiration", async () => { - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); - await expect(pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms)).to.emit(pool, "LogBorrow"); - }); - }); - - describeSnapshot("Remove Collateral", () => { - let pool: PrivatePoolNFT; - - before(async () => { - pool = await deployPool(); - for (const signer of [bob, carol]) { - await apes.connect(signer).setApprovalForAll(pool.address, true); - } - const share = getBigNumber(450); // 1000 guineas - await pool.connect(alice).addAsset(false, share); - - await addToken(pool, apeIds.bobOne, { - valuation: getBigNumber(1, 8), - expiration: nextYear, - annualInterestBPS: 10000, - }); - await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); - - await addToken(pool, apeIds.carolOne, { - valuation: getBigNumber(1, 8), - expiration: nextYear, - annualInterestBPS: 10000, - }); - await pool.connect(carol).addCollateral(apeIds.carolOne, carol.address, false); - }); - - it("Should let depositors withdraw their unused collateral", async () => { - await expect(pool.connect(bob).removeCollateral(apeIds.bobOne, carol.address)) - .to.emit(pool, "LogRemoveCollateral") - .withArgs(bob.address, carol.address, apeIds.bobOne) - .to.emit(apes, "Transfer") - .withArgs(pool.address, carol.address, apeIds.bobOne); - - const loanStatus = await pool.tokenLoan(apeIds.bobOne); - expect(loanStatus.borrower).to.equal(AddressZero); - expect(loanStatus.startTime).to.equal(0); - expect(loanStatus.status).to.equal(LoanStatus.INITIAL); - }); - - it("Should not allow withdrawals if the collateral is in use", async () => { - // The only legitimate reason to take used collateral is a "liquidation" - // where the loan has expired. This is only available to the lender: - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - await expect(pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the lender"); - }); - - it("Should not give out someone else's unused collateral", async () => { - await expect(pool.connect(bob).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the borrower"); - }); - - it("Should let only the lender seize expired collateral", async () => { - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); - - await expect(pool.connect(bob).removeCollateral(apeIds.carolOne, bob.address)).to.be.revertedWith("PrivatePool: not the lender"); - - // You cannot "seize" your own collateral either: - await expect(pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address)).to.be.revertedWith("PrivatePool: not the lender"); - - await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)) - .to.emit(pool, "LogRemoveCollateral") - .withArgs(carol.address, alice.address, apeIds.carolOne) - .to.emit(apes, "Transfer") - .withArgs(pool.address, alice.address, apeIds.carolOne); - }); - - it("Should only allow seizing expired collateral", async () => { - const terms = await pool.tokenLoanParams(apeIds.bobOne); - await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); - - await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)).to.be.revertedWith("PrivatePool: not expired"); - }); - - it("Should only allow seizing collateral used for a loan", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); - - await expect(pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address)).to.be.revertedWith("PrivatePool: not the borrower"); - }); - }); -}); From 5ad449e156a9ecf35ba1a13c012ce699d89ce42d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:53:38 +0100 Subject: [PATCH 034/107] Allow repayments from anyone --- contracts/NFTPair.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index fa7cb00d..ccbae5a6 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -331,7 +331,6 @@ contract NFTPair is BoringOwnable, IMasterContract { function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); - require(loan.borrower == msg.sender, "NFTPair: not the borrower"); TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; require(loanParams.expiration > block.timestamp, "NFTPair: loan expired"); @@ -362,10 +361,11 @@ contract NFTPair is BoringOwnable, IMasterContract { } // No underflow: PROTOCOL_FEE_BPS < BPS by construction. feesEarnedShare += feeShare; - bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); - collateral.transferFrom(address(this), msg.sender, tokenId); delete tokenLoan[tokenId]; + bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); + collateral.transferFrom(address(this), loan.borrower, tokenId); + emit LogRepay(from, tokenId); } From e4dac51d37b48e0caf68c16146bbe082ca578e44 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:26:52 +0100 Subject: [PATCH 035/107] Consistently case event messages --- contracts/NFTPair.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index ccbae5a6..812a3b63 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -158,7 +158,7 @@ contract NFTPair is BoringOwnable, IMasterContract { ) public { // Edge case: valuation can be zero. That effectively gifts the NFT and // is therefore a bad idea, but does not break the contract. - require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: Loan exists"); + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); if (skim) { require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); } else { @@ -206,7 +206,7 @@ contract NFTPair is BoringOwnable, IMasterContract { bool skim ) public { TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_REQUESTED, "NFTPair: Not available"); + require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); TokenLoanParams memory params = tokenLoanParams[tokenId]; // Valuation has to be an exact match, everything else must be at least From afc1e1abd2411f814b838ee58c95d07495575172 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:27:26 +0100 Subject: [PATCH 036/107] Reorder requestLoan parameters and add cook action --- contracts/NFTPair.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 812a3b63..2527b543 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -152,8 +152,8 @@ contract NFTPair is BoringOwnable, IMasterContract { /// @param skim True if the token has already been transfered function requestLoan( uint256 tokenId, - address to, TokenLoanParams memory params, + address to, bool skim ) public { // Edge case: valuation can be zero. That effectively gifts the NFT and @@ -468,6 +468,12 @@ contract NFTPair is BoringOwnable, IMasterContract { } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); + } else if (action == ACTION_REQUEST_LOAN) { + (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode( + datas[i], + (uint256, TokenLoanParams, address, bool) + ); + requestLoan(tokenId, params, to, skim); } else if (action == ACTION_BENTO_SETAPPROVAL) { (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], From 0d57880fb0e581b3899777690769a445af51bc74 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:27:41 +0100 Subject: [PATCH 037/107] Fix lend(): store lender --- contracts/NFTPair.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 2527b543..69eb3395 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -234,6 +234,7 @@ contract NFTPair is BoringOwnable, IMasterContract { // No overflow: addends (and result) must fit in BentoBox feesEarnedShare += protocolFeeShare; + loan.lender = msg.sender; loan.status = LOAN_OUTSTANDING; loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; From fdc3d4f2113fc0b38f49ca0125410aea6d53c4b6 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:27:36 +0100 Subject: [PATCH 038/107] Fix calculateInterest (and improve comments) --- contracts/NFTPair.sol | 126 ++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 69eb3395..7a3d2107 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -101,7 +101,7 @@ contract NFTPair is BoringOwnable, IMasterContract { uint256 private constant PROTOCOL_FEE_BPS = 1000; uint256 private constant OPEN_FEE_BPS = 100; uint256 private constant BPS = 10_000; - uint256 private constant YEAR = 3600 * 24 * 365; + uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; /// @notice The constructor is only used for the initial master contract. /// @notice Subsequent clones are initialised via `init`. @@ -248,84 +248,80 @@ contract NFTPair is BoringOwnable, IMasterContract { /// /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), /// - /// where time = t/YEAR, but once the result no longer fits in 128 bits it - /// may be very inaccurate. Which does not matter, because the BentoBox - /// cannot hold that high a balance. + /// where time = t/YEAR, up to at most the rounding error obtained in + /// calculating linear interest. /// - /// @param n Highest order term. Set n=1 (or 0) for linear interest only. + /// If the theoretical result that we are approximating (the rightmost part + /// of the above inquality) fits in 128 bits, then the function is + /// guaranteed not to revert (unless n > 250, which is way too high). + /// If even the linear interest (leftmost part of the inequality) does not + /// the function will revert. + /// Otherwise, the function may revert, return a reasonable result, or + /// return a very inaccurate result. Even then the above inequality is + /// respected. + /// + /// @param n Highest order term. Treats both 0 and 1 as "linear only". function calculateInterest( uint256 principal, - uint256 t, - uint256 aprBPS, - uint256 n + uint64 t, + uint16 aprBPS, + uint8 n ) public pure returns (uint256 interest) { - // These calculations can, in principle, overflow, given sufficiently - // ridiculous inputs, as shown in the following table: + // We calculate + // + // ----- n ----- n + // \ principal * (t * aprBPS)^k \ + // ) -------------------------- =: ) term_k + // / k! * YEAR_BPS^k / + // ----- k = 1 ----- k = 1 // - // principal = 2^128 - 1 (128 bits) - // t = 30,000 years (40 bits) - // interest = 655.35% APR (16 bits) + // which approaches, but never exceeds the "theoretical" result, // - // Even then, we will not see an overflow until after the fifth term: + // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 // - // k denom > 2^ term * x <= 2^ term * x / denom <= 2^ - // --------------------------------------------------------------- - // 1 38 128 + 56 = 184 184 - 38 = 146 - // 2 39 146 + 56 = 202 202 - 39 = 163 - // 3 40 163 + 56 = 219 219 - 40 = 179 - // 4 42 179 + 56 = 235 235 - 42 = 193 - // 5 45 193 + 56 = 249 249 - 45 = 204 + // as n goes to infinity. We use the fact that // - // (Denominator bits: floor (lg (k! * 10_000 * YEAR)) ) + // principal * (t * aprBPS)^(k-1) * (t * aprBPS) + // term_k = --------------------------------------------- + // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS // - // To be fair, five terms would not adequately capture the effects of - // this much compound interest over this time period. On the high end - // of actual usage we expect to see, it does, and there is no overflow: + // t * aprBPS + // = term_{k-1} * ------------ (*) + // k * YEAR_BPS // - // principal = 1 billion ether (1e27) (90 bits) - // t = 5 years (~158 million seconds) (28 bits) - // apr = 30% (12 bits) + // to calculate the terms one by one. The principal affords us the + // precision to carry out the division without resorting to fixed-point + // math. Any rounding error is downward, which we consider acceptable. // - // k denom > 2^ term * x <= 2^ term * x / denom <= 2^ - // --------------------------------------------------------------- - // 1 38 90 + 40 = 130 130 - 38 = 92 - // 2 39 92 + 40 = 132 132 - 39 = 93 - // 3 40 93 + 40 = 133 133 - 40 = 93 - // 4 42 93 + 40 = 133 133 - 42 = 91 + // Since all numbers involved are positive, each term is certainly + // bounded above by M. From (*) we see that any intermediate results + // are at most // - // ..and from here on, the terms keep getting smaller; the factorial in - // the denominator "wins". Indeed, the result is dominated by the "2" - // and "3" terms: the partial sums are: + // denom_k := k * YEAR_BPS. // - // n Σ_1..n (1.5^k / k!) - // -------------------------------------- - // 1 1.5 - // 2 2.625 - // 3 3.1875 - // 4 3.3984375 - // 5 3.46171875 - // ... - // (Infinity) 3.48168907... (e^1.5 - 1) + // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits, + // which proves that all calculations will certainly not overflow if M + // fits in 128 bits. // - // Finally: the denominator overflows at n = 51; n = 50 is "safe" - // but useless; if we need that many terms, interest is high enough - // to be unpayable. - // However, n >= 252 is not safe; 10_000 * YEAR * 252! = 0 mod 2^256. + // If M does not fit, then the intermediate results for some term may + // eventually overflow, but this cannot happen at the first term, and + // neither can the total overflow because it uses checked math. // - // Since even abnormal values will result in a few "valid" terms that - // are enough to make the interest unpayably high, it suffices to check - // that the total cannot go down (final `add`). If that calculation - // overflows, then reverting is no worse than anything else we may do. + // This constitutes a guarantee of specified behavior when M >= 2^128. // - uint256 x = t * aprBPS; - uint256 denom = YEAR * BPS; - uint256 term = (principal * x) / denom; - interest = term; + uint256 x = uint256(t) * aprBPS; + uint256 term_k = (principal * x) / YEAR_BPS; + uint256 denom_k = YEAR_BPS; + + interest = term_k; for (uint256 k = 2; k <= n; k++) { - term *= x; // Safe: See above. - denom *= k; // Fits up to k = 50; no problem after - term /= denom; // Safe until k = 251 - interest = interest.add(term); // <- Only overflow check we need + denom_k += YEAR_BPS; + term_k = (term_k * x) / denom_k; + interest = interest.add(term_k); // <- Only overflow check we need + } + + if (interest >= 2**128) { + revert(); } } @@ -335,17 +331,17 @@ contract NFTPair is BoringOwnable, IMasterContract { TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; require(loanParams.expiration > block.timestamp, "NFTPair: loan expired"); - uint256 principal = loanParams.valuation; + uint128 principal = loanParams.valuation; // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe: if this overflows, then all loans have expired anyway uint256 interest = calculateInterest( principal, - block.timestamp - loan.startTime, + uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS, loanParams.compoundInterestTerms ).to128(); uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; - // No overflow (both lines): to128() would have reverted amount = principal + interest; uint256 totalShare = bentoBox.toShare(asset, amount, false); From 6fbf3829dd276cb3e452e2b4aa6052922bb7615d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 22 Feb 2022 02:20:44 +0100 Subject: [PATCH 039/107] Add NFT tests (WIP) --- test/NFTPair.test.ts | 615 +++++++++++++++++++++++++++++++++++++++++++ test/PrivatePool.ts | 9 - 2 files changed, 615 insertions(+), 9 deletions(-) create mode 100644 test/NFTPair.test.ts diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts new file mode 100644 index 00000000..f19ed999 --- /dev/null +++ b/test/NFTPair.test.ts @@ -0,0 +1,615 @@ +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; +import { expect } from "chai"; +import { BigNumber, BigNumberish, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; + +import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; +import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, NFTPair } from "../typechain"; +import { describeSnapshot } from "./helpers"; +import { Cook, encodeLoanParamsNFT } from "./PrivatePool"; + +const LoanStatus = { + INITIAL: 0, + REQUESTED: 1, + OUTSTANDING: 2, +}; + +interface IDeployParams { + collateral: string; + asset: string; +} +interface IPartialDeployParams { + collateral?: string; + asset?: string; +} + +interface ILoanParams { + valuation: BigNumber; + expiration: number; + annualInterestBPS: number; + compoundInterestTerms: BigNumberish; +} +interface IPartialLoanParams { + valuation?: BigNumber; + expiration?: number; + annualInterestBPS?: number; + compoundInterestTerms?: BigNumberish; +} + +const { formatUnits } = ethers.utils; +const { MaxUint256, AddressZero, HashZero } = ethers.constants; + +const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; +const nextDecade = Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 10; + +describe("NFT Pair", async () => { + let apes: ERC721Mock; + let guineas: ERC20Mock; + let bentoBox: BentoBoxMock; + let masterContract: NFTPair; + let deployer: SignerWithAddress; + let alice: SignerWithAddress; + let bob: SignerWithAddress; + let carol: SignerWithAddress; + + // Named token IDs for testing.. + let apeIds: { + aliceOne: BigNumberish; + aliceTwo: BigNumberish; + bobOne: BigNumberish; + bobTwo: BigNumberish; + carolOne: BigNumberish; + carolTwo: BigNumberish; + }; + + const deployContract = async (name, ...args) => { + const contract = await ethers.getContractFactory(name).then((f) => f.deploy(...args)); + // Simpler way to "cast"? The above works as the result if we igore types.. + return ethers.getContractAt(name, contract.address); + }; + + const deployPair = async (options: IPartialDeployParams = {}) => { + const { collateral = apes.address, asset = guineas.address } = options; + const deployTx = await bentoBox + .deploy(masterContract.address, encodeParameters(["address", "address"], [collateral, asset]), false) + .then((tx) => tx.wait()); + for (const e of deployTx.events || []) { + if (e.eventSignature == "LogDeploy(address,bytes,address)") { + return ethers.getContractAt("NFTPair", e.args?.cloneAddress); + } + } + throw new Error("Deploy event not found"); // (For the typechecker..) + }; + + const addToken = (pool, tokenId, params: IPartialLoanParams) => + pool.connect(alice).updateLoanParams(tokenId, { + valuation: 0, + expiration: nextYear, + openFeeBPS: 1000, + annualInterestBPS: 2000, + compoundInterestTerms: 5, + ...params, + }); + + // Specific to the mock implementation.. + const mintApe = async (ownerAddress) => { + const id = await apes.totalSupply(); + await apes.mint(ownerAddress); + return id; + }; + + before(async () => { + const weth = await deployContract("WETH9Mock"); + bentoBox = await deployContract("BentoBoxMock", weth.address); + masterContract = await deployContract("NFTPair", bentoBox.address); + await bentoBox.whitelistMasterContract(masterContract.address, true); + apes = await deployContract("ERC721Mock"); + guineas = await deployContract("ERC20Mock", getBigNumber(1_000_000)); + + const addresses = await getNamedAccounts(); + deployer = await ethers.getSigner(addresses.deployer); + alice = await ethers.getSigner(addresses.alice); + bob = await ethers.getSigner(addresses.bob); + carol = await ethers.getSigner(addresses.carol); + + const mc = masterContract.address; + const hz = HashZero; + for (const signer of [alice, bob, carol]) { + const addr = signer.address; + const bb = bentoBox.connect(signer); + await bb.setMasterContractApproval(addr, mc, true, 0, hz, hz); + + await guineas.transfer(addr, getBigNumber(10_000)); + await guineas.connect(signer).approve(bentoBox.address, MaxUint256); + await bb.deposit(guineas.address, addr, addr, getBigNumber(3000), 0); + } + await guineas.approve(bentoBox.address, MaxUint256); + await bentoBox.addProfit(guineas.address, getBigNumber(11000)); + + // Guineas: 9000 in, 11k profit => 9k shares is 20k guineas. + // ---- alice: + // Guineas: 7000.0 + // Guineas (BentoBox): 6666.666666666666666666 (3000.0 shares) + + apeIds = { + aliceOne: await mintApe(alice.address), + aliceTwo: await mintApe(alice.address), + bobOne: await mintApe(bob.address), + bobTwo: await mintApe(bob.address), + carolOne: await mintApe(carol.address), + carolTwo: await mintApe(carol.address), + }; + }); + + describeSnapshot("Deployment", () => { + let pool: NFTPair; + + before(async () => { + pool = await deployPair(); + }); + + it("Should deploy with expected parameters", async () => { + expect(await pool.asset()).to.equal(guineas.address); + expect(await pool.collateral()).to.equal(apes.address); + }); + + it("Should reject bad settings", async () => { + await expect(deployPair({ collateral: AddressZero })).to.be.revertedWith("NFTPair: bad pair"); + }); + + it("Should refuse to initialize twice", async () => { + await expect(pool.init(encodeParameters(["address", "address"], [apes.address, guineas.address]))).to.be.revertedWith( + "NFTPair: already initialized" + ); + }); + }); + + describeSnapshot("Request Loan", () => { + let tomorrow: number; + let pair: NFTPair; + + before(async () => { + tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; + + pair = await deployPair(); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + }); + + it("Should let anyone with an NFT request a loan against it", async () => { + const params = { + valuation: getBigNumber(10), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)) + .to.emit(apes, "Transfer") + .withArgs(alice.address, pair.address, apeIds.aliceOne) + .to.emit(pair, "LogRequestLoan") + .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + }); + + it("Should let anyone with an NFT request a loan (skim)", async () => { + // The intended use case of skimming is one transaction; this is not that + // situation. But since we are the only one interacting with the contract + // the logic still works: + const params = { + valuation: getBigNumber(10), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + await apes.connect(alice).transferFrom(alice.address, pair.address, apeIds.aliceOne); + await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, true)).to.emit(pair, "LogRequestLoan"); + }); + + it("Should fail to skim if token not present", async () => { + const params = { + valuation: getBigNumber(10), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, true)).to.be.revertedWith("NFTPair: skim failed"); + }); + + it("Should refuse second request. Important if skimming!", async () => { + const params = { + valuation: getBigNumber(10), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)).to.emit(pair, "LogRequestLoan"); + await expect(pair.connect(bob).requestLoan(apeIds.aliceOne, params, bob.address, true)).to.be.revertedWith("NFTPair: loan exists"); + }); + + it("Should refuse loan requests without collateral", async () => { + const params = { + valuation: getBigNumber(10), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + await expect(pair.connect(alice).requestLoan(apeIds.bobOne, params, alice.address, false)).to.be.revertedWith("From not owner"); + }); + }); + + describeSnapshot("Lend", async () => { + let tomorrow: number; + let params1: ILoanParams; + let pair: NFTPair; + + before(async () => { + pair = await deployPair(); + tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; + + for (const signer of [alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + params1 = { + valuation: getBigNumber(1000), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + + await pair.connect(alice).requestLoan(apeIds.aliceOne, params1, alice.address, false); + + await pair.connect(bob).requestLoan(apeIds.bobOne, params1, bob.address, false); + + // One on behalf of someone else: + await pair.connect(bob).requestLoan(apeIds.bobTwo, params1, carol.address, false); + }); + + it("Should allow anyone to lend", async () => { + const totalShare = params1.valuation.mul(9).div(20); + const openFeeShare = totalShare.div(100); + const borrowShare = totalShare.sub(openFeeShare); + + await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, false)) + .to.emit(pair, "LogLend") + .withArgs(carol.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, carol.address, pair.address, totalShare) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, alice.address, borrowShare); + + const loan = await pair.tokenLoan(apeIds.aliceOne); + expect(loan.lender).to.equal(carol.address); + expect(loan.borrower).to.equal(alice.address); + expect(loan.status).to.equal(LoanStatus.OUTSTANDING); + }); + + it("Should allow anyone to lend (skim)", async () => { + const totalShare = params1.valuation.mul(9).div(20); + + await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, totalShare); + await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, true)).to.emit(pair, "LogLend"); + }); + + it("Should revert if skim amount is too low", async () => { + const totalShare = params1.valuation.mul(9).div(20); + const totalShareM1 = totalShare.sub(1); + + await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, totalShareM1); + await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, true)).to.be.revertedWith("NFTPair: skim too much"); + }); + + it("Should allow collateralizing a loan for someone else", async () => { + const totalShare = params1.valuation.mul(9).div(20); + const openFeeShare = totalShare.div(100); + const borrowShare = totalShare.sub(openFeeShare); + + // Loan was requested by Bob, but money and option to repay go to Carol: + await expect(pair.connect(alice).lend(apeIds.bobTwo, params1, false)) + .to.emit(pair, "LogLend") + .withArgs(alice.address, apeIds.bobTwo) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pair.address, totalShare) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, carol.address, borrowShare); + + const loan = await pair.tokenLoan(apeIds.bobTwo); + expect(loan.lender).to.equal(alice.address); + expect(loan.borrower).to.equal(carol.address); + }); + + it("Should lend if expiration is earlier than expected", async () => { + const later = { ...params1, expiration: params1.expiration + 1 }; + await expect(pair.connect(carol).lend(apeIds.aliceOne, later, false)).to.emit(pair, "LogLend"); + }); + + it("Should lend if interest is higher than expected", async () => { + const less = { + ...params1, + annualInterestBPS: params1.annualInterestBPS - 1, + }; + await expect(pair.connect(carol).lend(apeIds.aliceOne, less, false)).to.emit(pair, "LogLend"); + }); + + it("Should NOT lend if valuation is off", async () => { + const tooHigh = { ...params1, valuation: params1.valuation.add(1) }; + const tooLow = { ...params1, valuation: params1.valuation.sub(1) }; + + await expect(pair.connect(carol).lend(apeIds.aliceOne, tooHigh, false)).to.be.revertedWith("NFTPair: bad params"); + await expect(pair.connect(carol).lend(apeIds.aliceOne, tooLow, false)).to.be.revertedWith("NFTPair: bad params"); + }); + + it("Should NOT lend if expiration is later than expected", async () => { + const earlier = { ...params1, expiration: params1.expiration - 1 }; + await expect(pair.connect(carol).lend(apeIds.aliceOne, earlier, false)).to.be.revertedWith("NFTPair: bad params"); + }); + + it("Should NOT lend if interest is lower than expected", async () => { + const more = { + ...params1, + annualInterestBPS: params1.annualInterestBPS + 1, + }; + await expect(pair.connect(carol).lend(apeIds.aliceOne, more, false)).to.be.revertedWith("NFTPair: bad params"); + }); + + it("Should only lend against the same token once", async () => { + await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, false)).to.emit(pair, "LogLend"); + await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, false)).to.be.revertedWith("NFTPair: not available"); + }); + + it("Should only lend if a request was made with collateral", async () => { + await expect(pair.connect(carol).lend(apeIds.aliceTwo, params1, false)).to.be.revertedWith("NFTPair: not available"); + }); + }); + + describeSnapshot("Update Loan Params", () => { + let tomorrow: number; + let params1: ILoanParams; + let pair: NFTPair; + + before(async () => { + pair = await deployPair(); + tomorrow = Math.floor(new Date().getTime() / 1000) + 86400; + + for (const signer of [alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + params1 = { + valuation: getBigNumber(1000), + expiration: tomorrow, + annualInterestBPS: 2000, + compoundInterestTerms: 4, + }; + + await pair.connect(alice).requestLoan(apeIds.aliceOne, params1, alice.address, false); + }); + + it("Should allow borrowers any update to loan requests", async () => { + const data = [params1]; + const recordUpdate = (k, f) => { + const params = data[data.length - 1]; + data.push({ ...params, [k]: f(params[k]) }); + }; + recordUpdate("valuation", (v) => v.add(10)); + recordUpdate("valuation", (v) => v.sub(20_000_000)); + recordUpdate("annualInterestBPS", (i) => i - 400); + recordUpdate("annualInterestBPS", (i) => i + 300); + recordUpdate("expiration", (e) => e + 10_000); + recordUpdate("expiration", (e) => e - 98_765); + + for (const params of data) { + await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params)) + .to.emit(pair, "LogUpdateLoanParams") + .withArgs(apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + } + }); + + it("Should refuse updates to someone else's requests", async () => { + const params2 = { ...params1, expiration: params1.expiration + 2 }; + await expect(pair.connect(bob).updateLoanParams(apeIds.aliceOne, params2)).to.be.revertedWith("NFTPair: not the borrower"); + }); + + it("..even if you set the loan up for them", async () => { + const params2 = { ...params1, expiration: params1.expiration + 2 }; + await pair.connect(bob).requestLoan(apeIds.bobOne, params1, alice.address, false); + await expect(pair.connect(bob).updateLoanParams(apeIds.bobOne, params2)).to.be.revertedWith("NFTPair: not the borrower"); + }); + + it("Should refuse updates to nonexisting loans", async () => { + const params2 = { ...params1, expiration: params1.expiration + 2 }; + await expect(pair.connect(alice).updateLoanParams(apeIds.aliceTwo, params2)).to.be.revertedWith("NFTPair: no collateral"); + }); + + it("Should refuse non-lender updates to outstanding loans", async () => { + const params2 = { ...params1, expiration: params1.expiration - 2 }; + await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params2)).to.emit(pair, "LogUpdateLoanParams"); + + await pair.connect(carol).lend(apeIds.aliceOne, params2, false); + + // Borrower: + await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params1)).to.be.revertedWith("NFTPair: not the lender"); + + // Someone else: + await expect(pair.connect(bob).updateLoanParams(apeIds.aliceOne, params1)).to.be.revertedWith("NFTPair: not the lender"); + }); + + it("Should accept same or better conditions from lender", async () => { + const data = [params1]; + const recordUpdate = (k, f) => { + const params = data[data.length - 1]; + data.push({ ...params, [k]: f(params[k]) }); + }; + recordUpdate("valuation", (v) => v.sub(10)); + recordUpdate("annualInterestBPS", (i) => i - 400); + recordUpdate("expiration", (e) => e + 10_000); + + await pair.connect(carol).lend(apeIds.aliceOne, params1, false); + + for (const params of data) { + await expect(pair.connect(carol).updateLoanParams(apeIds.aliceOne, params)).to.emit(pair, "LogUpdateLoanParams"); + } + }); + + it("Should refuse worse conditions from lender", async () => { + const data = []; + const recordUpdate = (k, f) => { + data.push({ ...params1, [k]: f(params1[k]) }); + }; + recordUpdate("valuation", (v) => v.add(1)); + recordUpdate("annualInterestBPS", (i) => i + 1); + recordUpdate("expiration", (e) => e - 1); + + await pair.connect(carol).lend(apeIds.aliceOne, params1, false); + + for (const params of data) { + await expect(pair.connect(carol).updateLoanParams(apeIds.aliceOne, params)).to.be.revertedWith("NFTPair: worse params"); + } + }); + }); + + // describeSnapshot("Remove Collateral", () => { + // let pool: NFTPair; + + // before(async () => { + // pool = await deployPair(); + // for (const signer of [bob, carol]) { + // await apes.connect(signer).setApprovalForAll(pool.address, true); + // } + // const share = getBigNumber(450); // 1000 guineas + // await pool.connect(alice).addAsset(false, share); + + // await addToken(pool, apeIds.bobOne, { + // valuation: getBigNumber(1, 8), + // expiration: nextYear, + // annualInterestBPS: 10000, + // }); + // await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); + + // await addToken(pool, apeIds.carolOne, { + // valuation: getBigNumber(1, 8), + // expiration: nextYear, + // annualInterestBPS: 10000, + // }); + // await pool + // .connect(carol) + // .addCollateral(apeIds.carolOne, carol.address, false); + // }); + + // it("Should let depositors withdraw their unused collateral", async () => { + // await expect( + // pool.connect(bob).removeCollateral(apeIds.bobOne, carol.address) + // ) + // .to.emit(pool, "LogRemoveCollateral") + // .withArgs(bob.address, carol.address, apeIds.bobOne) + // .to.emit(apes, "Transfer") + // .withArgs(pool.address, carol.address, apeIds.bobOne); + + // const loanStatus = await pool.tokenLoan(apeIds.bobOne); + // expect(loanStatus.borrower).to.equal(AddressZero); + // expect(loanStatus.startTime).to.equal(0); + // expect(loanStatus.status).to.equal(LoanStatus.INITIAL); + // }); + + // it("Should not allow withdrawals if the collateral is in use", async () => { + // // The only legitimate reason to take used collateral is a "liquidation" + // // where the loan has expired. This is only available to the lender: + // const terms = await pool.tokenLoanParams(apeIds.bobOne); + // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + // await expect( + // pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address) + // ).to.be.revertedWith("NFTPair: not the lender"); + // }); + + // it("Should not give out someone else's unused collateral", async () => { + // await expect( + // pool.connect(bob).removeCollateral(apeIds.carolOne, carol.address) + // ).to.be.revertedWith("NFTPair: not the borrower"); + // }); + + // it("Should let only the lender seize expired collateral", async () => { + // const terms = await pool.tokenLoanParams(apeIds.bobOne); + // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); + + // await expect( + // pool.connect(bob).removeCollateral(apeIds.carolOne, bob.address) + // ).to.be.revertedWith("NFTPair: not the lender"); + + // // You cannot "seize" your own collateral either: + // await expect( + // pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address) + // ).to.be.revertedWith("NFTPair: not the lender"); + + // await expect( + // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) + // ) + // .to.emit(pool, "LogRemoveCollateral") + // .withArgs(carol.address, alice.address, apeIds.carolOne) + // .to.emit(apes, "Transfer") + // .withArgs(pool.address, alice.address, apeIds.carolOne); + // }); + + // it("Should only allow seizing expired collateral", async () => { + // const terms = await pool.tokenLoanParams(apeIds.bobOne); + // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); + // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); + + // await expect( + // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) + // ).to.be.revertedWith("NFTPair: not expired"); + // }); + + // it("Should only allow seizing collateral used for a loan", async () => { + // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); + + // await expect( + // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) + // ).to.be.revertedWith("NFTPair: not the borrower"); + // }); + // }); + + // describeSnapshot("Repay", () => { + // let pool: NFTPair; + // let bobBorrowedAt: number; + // let carolBorrowedAt: number; + + // before(async () => { + // pool = await deployPair(); + // for (const signer of [bob, carol]) { + // await apes.connect(signer).setApprovalForAll(pool.address, true); + // } + // const share = getBigNumber(450); // 1000 guineas + // await pool.connect(alice).addAsset(false, share); + + // // 30% interest + // await addToken(pool, apeIds.bobOne, { + // valuation: getBigNumber(1, 8), + // expiration: nextYear, + // annualInterestBPS: 3_000, + // }); + // await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); + // const terms1 = await pool.tokenLoanParams(apeIds.bobOne); + // await pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms1); + // bobBorrowedAt = (await ethers.provider.getBlock("latest")).timestamp; + + // // 100% interest + // await addToken(pool, apeIds.carolOne, { + // valuation: getBigNumber(1, 18), + // expiration: nextDecade, + // annualInterestBPS: 10_000, + // }); + // await pool + // .connect(carol) + // .addCollateral(apeIds.carolOne, carol.address, false); + // const terms2 = await pool.tokenLoanParams(apeIds.carolOne); + // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms2); + // carolBorrowedAt = (await ethers.provider.getBlock("latest")).timestamp; + // }); + + // it("Should let borrowers repay debt", async () => { + // await advanceNextTime(123456); + // }); + // }); +}); diff --git a/test/PrivatePool.ts b/test/PrivatePool.ts index 275e1647..39a2e4a7 100644 --- a/test/PrivatePool.ts +++ b/test/PrivatePool.ts @@ -8,7 +8,6 @@ const makeStructTypeString = (namedTypes) => `tuple(${_.map(namedTypes, (t, name const loanParams = { valuation: "uint128", expiration: "uint64", - openFeeBPS: "uint16", annualInterestBPS: "uint16", compoundInterestTerms: "uint8", }; @@ -43,14 +42,6 @@ export const encodeInitData = makeStructEncoder({ LIQUIDATION_SEIZE_COLLATERAL: "bool", }); -export const encodeInitDataNFT = makeStructEncoder({ - collateral: "address", - asset: "address", - lender: "address", - tokenIds: "uint256[]", - loanParams: loanParamsArrayType, -}); - export const encodeLoanParamsNFT = makeStructEncoder(loanParams); // Cook actions From 243921fe90b9cd76bcf3898e178ca3aac9fea106 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 5 Mar 2022 03:27:56 +0100 Subject: [PATCH 040/107] Hardcode COMPOUND_INTEREST_TERMS --- contracts/NFTPair.sol | 96 ++++++++++++++++++++++++++++--------------- test/NFTPair.test.ts | 14 +------ 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 7a3d2107..2abca8cc 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -36,21 +36,8 @@ contract NFTPair is BoringOwnable, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan( - address indexed borrower, - uint256 indexed tokenId, - uint128 valuation, - uint64 expiration, - uint16 annualInterestBPS, - uint8 compoundInterestTerms - ); - event LogUpdateLoanParams( - uint256 indexed tokenId, - uint128 valuation, - uint64 expiration, - uint16 annualInterestBPS, - uint8 compoundInterestTerms - ); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 expiration, uint16 annualInterestBPS); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 expiration, uint16 annualInterestBPS); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request @@ -82,7 +69,6 @@ contract NFTPair is BoringOwnable, IMasterContract { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 expiration; // Pay before this or get liquidated uint16 annualInterestBPS; // Variable cost of taking out the loan - uint8 compoundInterestTerms; // Might as well. Stay under 50. } mapping(uint256 => TokenLoanParams) public tokenLoanParams; @@ -103,6 +89,57 @@ contract NFTPair is BoringOwnable, IMasterContract { uint256 private constant BPS = 10_000; uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; + // Highest order term in the Maclaurin series for exp used by + // `calculateIntest`. + // Intuitive interpretation: interest continuously accrues on the principal. + // That interest, in turn, earns "second-order" interest-on-interest, which + // itself earns "third-order" interest, etc. This constant determines how + // far we take this until we stop counting. + // + // The error, in terms of the interest rate, is at least + // + // ----- n ----- Infinity + // \ x^k \ x^k + // e^x - ) --- , which is ) --- , + // / k! / k! + // ----- k = 1 k ----- k = n + 1 + // + // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of + // interest that is owed at rate r over time t. It makes no difference if + // this is, say, 5%/year for 10 years, or 50% in one year; the calculation + // is the same. Why "at least"? There are also rounding errors. See + // `calculateInterest` for more detail. + // The factorial in the denominator "wins"; for all reasonable (and quite + // a few unreasonable) interest rates, the lower-order terms contribute the + // most to the total. The following table lists some of the calculated + // approximations for different values of n, along with the "true" result: + // + // Total: 10% 20% 50% 100% 200% 500% 1000% + // ----------------------------------------------------------------------- + // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0% + // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0% + // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7% + // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3% + // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7% + // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6% + // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3% + // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1% + // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3% + // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5% + // + // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6% + // + // For instance, calculating the compounding effects of 200% in "total" + // interest to the sixth order results in 635.6%, whereas the true result + // is 638.9%. + // At 500% that difference is a little more dramatic, but it is still in + // the same ballpark -- and of little practical consequence unless the + // collateral can be expected to go up more than 112 times in value. + // Still, for volatile tokens, or an asset that is somehow known to be very + // inflationary, use a different number. + // Zero (no interest at all) is ignored and treated as one (linear only). + uint8 private constant COMPOUND_INTEREST_TERMS = 6; + /// @notice The constructor is only used for the initial master contract. /// @notice Subsequent clones are initialised via `init`. constructor(IBentoBoxV1 bentoBox_) public { @@ -128,8 +165,7 @@ contract NFTPair is BoringOwnable, IMasterContract { require( params.expiration >= cur.expiration && params.valuation <= cur.valuation && - params.annualInterestBPS <= cur.annualInterestBPS && - params.compoundInterestTerms <= cur.compoundInterestTerms, + params.annualInterestBPS <= cur.annualInterestBPS, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -142,7 +178,7 @@ contract NFTPair is BoringOwnable, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + emit LogUpdateLoanParams(tokenId, params.valuation, params.expiration, params.annualInterestBPS); } /// @notice Deposit an NFT as collateral and request a loan against it @@ -170,7 +206,7 @@ contract NFTPair is BoringOwnable, IMasterContract { tokenLoan[tokenId] = loan; tokenLoanParams[tokenId] = params; - emit LogRequestLoan(to, tokenId, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + emit LogRequestLoan(to, tokenId, params.valuation, params.expiration, params.annualInterestBPS); } /// @notice Removes `tokenId` as collateral and transfers it to `to`. @@ -214,8 +250,7 @@ contract NFTPair is BoringOwnable, IMasterContract { require( params.valuation == accepted.valuation && params.expiration <= accepted.expiration && - params.annualInterestBPS >= accepted.annualInterestBPS && - params.compoundInterestTerms >= accepted.compoundInterestTerms, + params.annualInterestBPS >= accepted.annualInterestBPS, "NFTPair: bad params" ); @@ -259,14 +294,13 @@ contract NFTPair is BoringOwnable, IMasterContract { /// Otherwise, the function may revert, return a reasonable result, or /// return a very inaccurate result. Even then the above inequality is /// respected. - /// - /// @param n Highest order term. Treats both 0 and 1 as "linear only". function calculateInterest( uint256 principal, uint64 t, - uint16 aprBPS, - uint8 n + uint16 aprBPS ) public pure returns (uint256 interest) { + // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS) + // // We calculate // // ----- n ----- n @@ -308,13 +342,12 @@ contract NFTPair is BoringOwnable, IMasterContract { // neither can the total overflow because it uses checked math. // // This constitutes a guarantee of specified behavior when M >= 2^128. - // uint256 x = uint256(t) * aprBPS; uint256 term_k = (principal * x) / YEAR_BPS; uint256 denom_k = YEAR_BPS; interest = term_k; - for (uint256 k = 2; k <= n; k++) { + for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { denom_k += YEAR_BPS; term_k = (term_k * x) / denom_k; interest = interest.add(term_k); // <- Only overflow check we need @@ -335,12 +368,7 @@ contract NFTPair is BoringOwnable, IMasterContract { // No underflow: loan.startTime is only ever set to a block timestamp // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest( - principal, - uint64(block.timestamp - loan.startTime), - loanParams.annualInterestBPS, - loanParams.compoundInterestTerms - ).to128(); + uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; amount = principal + interest; diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index f19ed999..e000ba44 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -27,13 +27,11 @@ interface ILoanParams { valuation: BigNumber; expiration: number; annualInterestBPS: number; - compoundInterestTerms: BigNumberish; } interface IPartialLoanParams { valuation?: BigNumber; expiration?: number; annualInterestBPS?: number; - compoundInterestTerms?: BigNumberish; } const { formatUnits } = ethers.utils; @@ -87,7 +85,6 @@ describe("NFT Pair", async () => { expiration: nextYear, openFeeBPS: 1000, annualInterestBPS: 2000, - compoundInterestTerms: 5, ...params, }); @@ -183,13 +180,12 @@ describe("NFT Pair", async () => { valuation: getBigNumber(10), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)) .to.emit(apes, "Transfer") .withArgs(alice.address, pair.address, apeIds.aliceOne) .to.emit(pair, "LogRequestLoan") - .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS); }); it("Should let anyone with an NFT request a loan (skim)", async () => { @@ -200,7 +196,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(10), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await apes.connect(alice).transferFrom(alice.address, pair.address, apeIds.aliceOne); await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, true)).to.emit(pair, "LogRequestLoan"); @@ -211,7 +206,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(10), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, true)).to.be.revertedWith("NFTPair: skim failed"); }); @@ -221,7 +215,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(10), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)).to.emit(pair, "LogRequestLoan"); await expect(pair.connect(bob).requestLoan(apeIds.aliceOne, params, bob.address, true)).to.be.revertedWith("NFTPair: loan exists"); @@ -232,7 +225,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(10), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await expect(pair.connect(alice).requestLoan(apeIds.bobOne, params, alice.address, false)).to.be.revertedWith("From not owner"); }); @@ -255,7 +247,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(1000), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await pair.connect(alice).requestLoan(apeIds.aliceOne, params1, alice.address, false); @@ -380,7 +371,6 @@ describe("NFT Pair", async () => { valuation: getBigNumber(1000), expiration: tomorrow, annualInterestBPS: 2000, - compoundInterestTerms: 4, }; await pair.connect(alice).requestLoan(apeIds.aliceOne, params1, alice.address, false); @@ -402,7 +392,7 @@ describe("NFT Pair", async () => { for (const params of data) { await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params)) .to.emit(pair, "LogUpdateLoanParams") - .withArgs(apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS, params.compoundInterestTerms); + .withArgs(apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS); } }); From 1d43ef37b9466c3791aa11625fdc99bb03240d6c Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 11 Mar 2022 02:30:35 +0100 Subject: [PATCH 041/107] Signed lend/borrow --- contracts/NFTPair.sol | 140 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 18 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 2abca8cc..3b14d051 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -21,6 +21,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "@boringcrypto/boring-solidity/contracts/Domain.sol"; import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; @@ -30,7 +31,7 @@ import "./interfaces/IERC721.sol"; /// @title NFTPair /// @dev This contract allows contract calls to any contract (except BentoBox) /// from arbitrary callers thus, don't trust calls from this contract in any circumstances. -contract NFTPair is BoringOwnable, IMasterContract { +contract NFTPair is BoringOwnable, Domain, IMasterContract { using BoringMath for uint256; using BoringMath128 for uint128; using RebaseLibrary for Rebase; @@ -140,6 +141,9 @@ contract NFTPair is BoringOwnable, IMasterContract { // Zero (no interest at all) is ignored and treated as one (linear only). uint8 private constant COMPOUND_INTEREST_TERMS = 6; + // For signed lend / borrow requests: + mapping(address => uint256) public nonces; + /// @notice The constructor is only used for the initial master contract. /// @notice Subsequent clones are initialised via `init`. constructor(IBentoBoxV1 bentoBox_) public { @@ -181,24 +185,20 @@ contract NFTPair is BoringOwnable, IMasterContract { emit LogUpdateLoanParams(tokenId, params.valuation, params.expiration, params.annualInterestBPS); } - /// @notice Deposit an NFT as collateral and request a loan against it - /// @param tokenId ID of the NFT - /// @param to Address to receive the loan, or option to withdraw collateral - /// @param params Loan conditions on offer - /// @param skim True if the token has already been transfered - function requestLoan( + function _requestLoan( + address collateralProvider, uint256 tokenId, TokenLoanParams memory params, address to, bool skim - ) public { + ) private { // Edge case: valuation can be zero. That effectively gifts the NFT and // is therefore a bad idea, but does not break the contract. require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); if (skim) { require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); } else { - collateral.transferFrom(msg.sender, address(this), tokenId); + collateral.transferFrom(collateralProvider, address(this), tokenId); } TokenLoan memory loan; loan.borrower = to; @@ -209,6 +209,20 @@ contract NFTPair is BoringOwnable, IMasterContract { emit LogRequestLoan(to, tokenId, params.valuation, params.expiration, params.annualInterestBPS); } + /// @notice Deposit an NFT as collateral and request a loan against it + /// @param tokenId ID of the NFT + /// @param to Address to receive the loan, or option to withdraw collateral + /// @param params Loan conditions on offer + /// @param skim True if the token has already been transfered + function requestLoan( + uint256 tokenId, + TokenLoanParams memory params, + address to, + bool skim + ) public { + _requestLoan(msg.sender, tokenId, params, to, skim); + } + /// @notice Removes `tokenId` as collateral and transfers it to `to`. /// @notice This destroys the loan. /// @param tokenId The token @@ -232,15 +246,13 @@ contract NFTPair is BoringOwnable, IMasterContract { emit LogRemoveCollateral(tokenId, to); } - /// @notice Lends with the parameters specified by the borrower. - /// @param tokenId ID of the token that will function as collateral - /// @param accepted Loan parameters as the lender saw them, for security - /// @param skim True if the assets have been transfered to the cauldron - function lend( + // Assumes the lender has agreed to the loan. + function _lend( + address lender, uint256 tokenId, TokenLoanParams memory accepted, bool skim - ) public { + ) internal { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); TokenLoanParams memory params = tokenLoanParams[tokenId]; @@ -261,7 +273,7 @@ contract NFTPair is BoringOwnable, IMasterContract { if (skim) { require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); } else { - bentoBox.transfer(asset, msg.sender, address(this), totalShare); + bentoBox.transfer(asset, lender, address(this), totalShare); } // No underflow: follows from OPEN_FEE_BPS <= BPS uint256 borrowerShare = totalShare - protocolFeeShare; @@ -269,12 +281,104 @@ contract NFTPair is BoringOwnable, IMasterContract { // No overflow: addends (and result) must fit in BentoBox feesEarnedShare += protocolFeeShare; - loan.lender = msg.sender; + loan.lender = lender; loan.status = LOAN_OUTSTANDING; loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; - emit LogLend(msg.sender, tokenId); + emit LogLend(lender, tokenId); + } + + /// @notice Lends with the parameters specified by the borrower. + /// @param tokenId ID of the token that will function as collateral + /// @param accepted Loan parameters as the lender saw them, for security + /// @param skim True if the funds have been transfered to the contract + function lend( + uint256 tokenId, + TokenLoanParams memory accepted, + bool skim + ) public { + _lend(msg.sender, tokenId, accepted, skim); + } + + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + // keccak256("Lend(uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x86c5b2291647820714f2a8238e26e034e2529cf064e0e025f91b989105b586b1; + + // keccak256("Borrow(uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0x697411e8541f6948a270862fd6b9dfd84c5d5cf563031f2962d2572f8163b155; + + /// @notice Request and immediately borrow from a pre-committed lender + + /// @notice Caller provides collateral; loan can go to a different address. + /// @param tokenId ID of the token that will function as collateral + /// @param lender Lender, whose BentoBox balance the funds will come from + /// @param recipient Address to receive the loan. + /// @param params Loan parameters requested, and signed by the lender + /// @param skimCollateral True if the collateral has already been transfered + function requestAndBorrow( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + require(block.timestamp <= deadline, "NFTPair: signature expired"); + bytes32 dataHash = keccak256( + abi.encode( + LEND_SIGNATURE_HASH, + tokenId, + params.valuation, + params.expiration, + params.annualInterestBPS, + nonces[lender]++, + deadline + ) + ); + require(ecrecover(_getDigest(dataHash), v, r, s) == lender, "NFTPair: signature invalid"); + _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); + _lend(lender, tokenId, params, false); + } + + /// @notice Take collateral from a pre-commited borrower and lend against it + /// @notice Collateral must come from the borrower, not a third party. + /// @param tokenId ID of the token that will function as collateral + /// @param borrower Address that provides collateral and receives the loan + /// @param params Loan terms offered, and signed by the borrower + /// @param skimFunds True if the funds have been transfered to the contract + function takeCollateralAndLend( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public { + require(block.timestamp <= deadline, "NFTPair: signature expired"); + bytes32 dataHash = keccak256( + abi.encode( + BORROW_SIGNATURE_HASH, + tokenId, + params.valuation, + params.expiration, + params.annualInterestBPS, + nonces[borrower]++, + deadline + ) + ); + require(ecrecover(_getDigest(dataHash), v, r, s) == borrower, "NFTPair: signature invalid"); + _requestLoan(borrower, tokenId, params, borrower, false); + _lend(msg.sender, tokenId, params, skimFunds); } /// Approximates continuous compounding. Uses Horner's method to evaluate From 40e2a982cd5e441fad115c8aee8bc537c3bf1826 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 12 Mar 2022 19:37:46 +0100 Subject: [PATCH 042/107] Add some tests for recent changes (partial) --- test/NFTPair.test.ts | 66 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index e000ba44..1ffa9450 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -377,7 +377,7 @@ describe("NFT Pair", async () => { }); it("Should allow borrowers any update to loan requests", async () => { - const data = [params1]; + const data: ILoanParams[] = [params1]; const recordUpdate = (k, f) => { const params = data[data.length - 1]; data.push({ ...params, [k]: f(params[k]) }); @@ -443,7 +443,7 @@ describe("NFT Pair", async () => { }); it("Should refuse worse conditions from lender", async () => { - const data = []; + const data: ILoanParams[] = []; const recordUpdate = (k, f) => { data.push({ ...params1, [k]: f(params1[k]) }); }; @@ -459,6 +459,68 @@ describe("NFT Pair", async () => { }); }); + describeSnapshot("Remove Collateral", () => { + let pair: NFTPair; + const params: ILoanParams = { + valuation: getBigNumber(123), + annualInterestBPS: 10_000, + expiration: Math.floor(new Date().getTime() / 1000) + 86400, + }; + const valuationShare = params.valuation.mul(9).div(20); + + before(async () => { + pair = await deployPair(); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + for (const id of [apeIds.aliceOne, apeIds.aliceTwo]) { + await pair.connect(alice).requestLoan(id, params, alice.address, false); + } + await pair.connect(bob).lend(apeIds.aliceOne, params, false); + }); + + it("Should allow borrowers to remove unused collateral", async () => { + await expect(pair.connect(alice).removeCollateral(apeIds.aliceTwo, alice.address)) + .to.emit(pair, "LogRemoveCollateral") + .withArgs(apeIds.aliceTwo, alice.address) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceTwo); + }); + + it("Should not allow others to remove unused collateral", async () => { + await expect(pair.connect(bob).removeCollateral(apeIds.aliceTwo, alice.address)).to.be.revertedWith("NFTPair: not the borrower"); + }); + + it("Should not allow borrowers to remove used collateral", async () => { + await expect(pair.connect(alice).removeCollateral(apeIds.aliceOne, alice.address)).to.be.revertedWith("NFTPair: not the lender"); + }); + + it("Should allow lenders to seize collateral upon expiry", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + // Send it to someone else for a change: + await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)) + .to.emit(pair, "LogRemoveCollateral") + .withArgs(apeIds.aliceOne, carol.address) + .to.emit(apes, "Transfer") + .withArgs(pair.address, carol.address, apeIds.aliceOne); + }); + + it("Should not allow lenders to seize collateral otherwise", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration - 1]); + await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not expired"); + }); + + it("Should not allow others to seize collateral ever", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration - 1]); + await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); + + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration + 1_000_000]); + await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); + }); + }); + // describeSnapshot("Remove Collateral", () => { // let pool: NFTPair; From c06426cc7ad543f2ffdd27aebb35bd33b2688142 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 12 Mar 2022 19:37:51 +0100 Subject: [PATCH 043/107] Make feesEarnedShare public --- contracts/NFTPair.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 3b14d051..263b4ad1 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -63,7 +63,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // "Shares" are BentoBox shares. // Track assets we own. Used to allow skimming the excesss. - uint256 feesEarnedShare; + uint256 public feesEarnedShare; // Per token settings. struct TokenLoanParams { From 92d1efd5ee58d9484555be3e1ae39c6d987eb639 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 16 Mar 2022 05:38:15 +0100 Subject: [PATCH 044/107] Index tokenId in LogRepay event --- contracts/NFTPair.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 263b4ad1..bd34b3b9 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -43,7 +43,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request event LogLend(address indexed lender, uint256 indexed tokenId); - event LogRepay(address indexed from, uint256 tokenId); + event LogRepay(address indexed from, uint256 indexed tokenId); event LogFeeTo(address indexed newFeeTo); event LogWithdrawFees(address indexed feeTo, uint256 feeShare); From df2b5f364ab74613496264d0d720e5a820f59d9c Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 16 Mar 2022 05:38:21 +0100 Subject: [PATCH 045/107] (Remove old TODO comments) --- contracts/NFTPair.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index bd34b3b9..eeb8b6ca 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -158,7 +158,6 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { require(address(collateral) != address(0), "NFTPair: bad pair"); } - // TODO: Somehow merge this with `updateLoanParams` function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { @@ -178,7 +177,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.borrower, "NFTPair: not the borrower"); } else { // The loan has not been taken out yet; the borrower needs to - // provide collateral. (TODO: Do that here?) + // provide collateral. revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; From 12f517b4d4529ae002275e523226689e0079080d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 16 Mar 2022 05:38:27 +0100 Subject: [PATCH 046/107] Add tests for repay() --- test/NFTPair.test.ts | 422 ++++++++++++++++++++++++++++--------------- utilities/index.ts | 1 + utilities/math.ts | 96 ++++++++++ 3 files changed, 373 insertions(+), 146 deletions(-) create mode 100644 utilities/math.ts diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 1ffa9450..baa871e6 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { BigNumber, BigNumberish, Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; -import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; +import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; import { Cook, encodeLoanParamsNFT } from "./PrivatePool"; @@ -36,6 +36,8 @@ interface IPartialLoanParams { const { formatUnits } = ethers.utils; const { MaxUint256, AddressZero, HashZero } = ethers.constants; +// This one was not defined.. +const MaxUint128 = BigNumber.from(2).pow(128).sub(1); const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; const nextDecade = Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 10; @@ -101,7 +103,7 @@ describe("NFT Pair", async () => { masterContract = await deployContract("NFTPair", bentoBox.address); await bentoBox.whitelistMasterContract(masterContract.address, true); apes = await deployContract("ERC721Mock"); - guineas = await deployContract("ERC20Mock", getBigNumber(1_000_000)); + guineas = await deployContract("ERC20Mock", MaxUint256); const addresses = await getNamedAccounts(); deployer = await ethers.getSigner(addresses.deployer); @@ -466,7 +468,6 @@ describe("NFT Pair", async () => { annualInterestBPS: 10_000, expiration: Math.floor(new Date().getTime() / 1000) + 86400, }; - const valuationShare = params.valuation.mul(9).div(20); before(async () => { pair = await deployPair(); @@ -521,147 +522,276 @@ describe("NFT Pair", async () => { }); }); - // describeSnapshot("Remove Collateral", () => { - // let pool: NFTPair; - - // before(async () => { - // pool = await deployPair(); - // for (const signer of [bob, carol]) { - // await apes.connect(signer).setApprovalForAll(pool.address, true); - // } - // const share = getBigNumber(450); // 1000 guineas - // await pool.connect(alice).addAsset(false, share); - - // await addToken(pool, apeIds.bobOne, { - // valuation: getBigNumber(1, 8), - // expiration: nextYear, - // annualInterestBPS: 10000, - // }); - // await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); - - // await addToken(pool, apeIds.carolOne, { - // valuation: getBigNumber(1, 8), - // expiration: nextYear, - // annualInterestBPS: 10000, - // }); - // await pool - // .connect(carol) - // .addCollateral(apeIds.carolOne, carol.address, false); - // }); - - // it("Should let depositors withdraw their unused collateral", async () => { - // await expect( - // pool.connect(bob).removeCollateral(apeIds.bobOne, carol.address) - // ) - // .to.emit(pool, "LogRemoveCollateral") - // .withArgs(bob.address, carol.address, apeIds.bobOne) - // .to.emit(apes, "Transfer") - // .withArgs(pool.address, carol.address, apeIds.bobOne); - - // const loanStatus = await pool.tokenLoan(apeIds.bobOne); - // expect(loanStatus.borrower).to.equal(AddressZero); - // expect(loanStatus.startTime).to.equal(0); - // expect(loanStatus.status).to.equal(LoanStatus.INITIAL); - // }); - - // it("Should not allow withdrawals if the collateral is in use", async () => { - // // The only legitimate reason to take used collateral is a "liquidation" - // // where the loan has expired. This is only available to the lender: - // const terms = await pool.tokenLoanParams(apeIds.bobOne); - // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - // await expect( - // pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address) - // ).to.be.revertedWith("NFTPair: not the lender"); - // }); - - // it("Should not give out someone else's unused collateral", async () => { - // await expect( - // pool.connect(bob).removeCollateral(apeIds.carolOne, carol.address) - // ).to.be.revertedWith("NFTPair: not the borrower"); - // }); - - // it("Should let only the lender seize expired collateral", async () => { - // const terms = await pool.tokenLoanParams(apeIds.bobOne); - // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); - - // await expect( - // pool.connect(bob).removeCollateral(apeIds.carolOne, bob.address) - // ).to.be.revertedWith("NFTPair: not the lender"); - - // // You cannot "seize" your own collateral either: - // await expect( - // pool.connect(carol).removeCollateral(apeIds.carolOne, carol.address) - // ).to.be.revertedWith("NFTPair: not the lender"); - - // await expect( - // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) - // ) - // .to.emit(pool, "LogRemoveCollateral") - // .withArgs(carol.address, alice.address, apeIds.carolOne) - // .to.emit(apes, "Transfer") - // .withArgs(pool.address, alice.address, apeIds.carolOne); - // }); - - // it("Should only allow seizing expired collateral", async () => { - // const terms = await pool.tokenLoanParams(apeIds.bobOne); - // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms); - // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear - 1]); - - // await expect( - // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) - // ).to.be.revertedWith("NFTPair: not expired"); - // }); - - // it("Should only allow seizing collateral used for a loan", async () => { - // await ethers.provider.send("evm_setNextBlockTimestamp", [nextYear]); - - // await expect( - // pool.connect(alice).removeCollateral(apeIds.carolOne, alice.address) - // ).to.be.revertedWith("NFTPair: not the borrower"); - // }); - // }); - - // describeSnapshot("Repay", () => { - // let pool: NFTPair; - // let bobBorrowedAt: number; - // let carolBorrowedAt: number; - - // before(async () => { - // pool = await deployPair(); - // for (const signer of [bob, carol]) { - // await apes.connect(signer).setApprovalForAll(pool.address, true); - // } - // const share = getBigNumber(450); // 1000 guineas - // await pool.connect(alice).addAsset(false, share); - - // // 30% interest - // await addToken(pool, apeIds.bobOne, { - // valuation: getBigNumber(1, 8), - // expiration: nextYear, - // annualInterestBPS: 3_000, - // }); - // await pool.connect(bob).addCollateral(apeIds.bobOne, bob.address, false); - // const terms1 = await pool.tokenLoanParams(apeIds.bobOne); - // await pool.connect(bob).borrow(apeIds.bobOne, bob.address, terms1); - // bobBorrowedAt = (await ethers.provider.getBlock("latest")).timestamp; - - // // 100% interest - // await addToken(pool, apeIds.carolOne, { - // valuation: getBigNumber(1, 18), - // expiration: nextDecade, - // annualInterestBPS: 10_000, - // }); - // await pool - // .connect(carol) - // .addCollateral(apeIds.carolOne, carol.address, false); - // const terms2 = await pool.tokenLoanParams(apeIds.carolOne); - // await pool.connect(carol).borrow(apeIds.carolOne, carol.address, terms2); - // carolBorrowedAt = (await ethers.provider.getBlock("latest")).timestamp; - // }); - - // it("Should let borrowers repay debt", async () => { - // await advanceNextTime(123456); - // }); - // }); + describeSnapshot("Repay", () => { + let pair: NFTPair; + + const DAY = 24 * 3600; + const YEAR = 365 * DAY; + const params: ILoanParams = { + valuation: getBigNumber(1), + annualInterestBPS: 10_000, + expiration: Math.floor(new Date().getTime() / 1000) + YEAR, + }; + const valuationShare = params.valuation.mul(9).div(20); + const borrowerShare = valuationShare.mul(99).div(100); + + // Theoretically this could fail to actually bound the repay share because + // of the FP math used. Double check using a more exact method if that + // happens: + const YEAR_BPS = YEAR * 10_000; + const COMPOUND_TERMS = 6; + const getMaxRepayShare = (time, params_) => { + // We mimic what the contract does, but without rounding errors in the + // approximation, so the upper bound should be strict. + // 1. Calculate exact amount owed; round it down, like the contract does. + // 2. Convert that to Bento shares (still hardcoded at 9/20); rounding up + const x = BigRational.from(time * params_.annualInterestBPS).div(YEAR_BPS); + return expApprox(x, COMPOUND_TERMS).mul(params_.valuation).floor().mul(9).add(19).div(20); + }; + + before(async () => { + pair = await deployPair(); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + for (const id of [apeIds.aliceOne, apeIds.aliceTwo]) { + await pair.connect(alice).requestLoan(id, params, alice.address, false); + } + await pair.connect(bob).lend(apeIds.aliceOne, params, false); + }); + + it("Should allow borrowers to pay off loans before expiry", async () => { + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + // Two Bento transfers: payment to the lender, fee to the contract + await advanceNextTime(DAY); + await expect(pair.connect(alice).repay(apeIds.aliceOne, false)) + .to.emit(pair, "LogRepay") + .withArgs(alice.address, apeIds.aliceOne) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(DAY, params); + const linearInterest = valuationShare.mul(params.annualInterestBPS).mul(DAY).div(YEAR_BPS); + + const paid = t0.alice.sub(t1.alice); + expect(paid).to.be.gte(valuationShare.add(linearInterest)); + expect(paid).to.be.lte(maxRepayShare); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + // The fee is hardcoded at 10% of the interest + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.gte(linearInterest); + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); + }); + + it("Should allow paying off loans for someone else", async () => { + // ..and take from the correct person: + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + carol: await bentoBox.balanceOf(guineas.address, carol.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + await advanceNextTime(DAY); + await expect(pair.connect(carol).repay(apeIds.aliceOne, false)) + .to.emit(pair, "LogRepay") + .withArgs(carol.address, apeIds.aliceOne) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(DAY, params); + + // Alice paid or received nothing: + expect(t0.alice).to.equal(t1.alice); + + const paid = t0.carol.sub(t1.carol); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); + }); + + it("Should allow paying off loans for someone else (skim)", async () => { + const interval = 234 * DAY + 5678; + // Does not matter who supplies the payment. Note that there will be + // an excess left; skimming is really only suitable for contracts that + // can calculate the exact repayment needed: + const exactAmount = params.valuation.add(await pair.calculateInterest(params.valuation, interval, params.annualInterestBPS)); + // The contract rounds down; we round up and add a little: + const closeToShare = exactAmount.mul(9).add(19).div(20); + const enoughShare = closeToShare.add(getBigNumber(1337, 8)); + + // This would normally be done in the same transaction... + await bentoBox.connect(bob).transfer(guineas.address, bob.address, pair.address, enoughShare); + + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + carol: await bentoBox.balanceOf(guineas.address, carol.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); + await expect(pair.connect(carol).repay(apeIds.aliceOne, true)) + .to.emit(pair, "LogRepay") + .withArgs(pair.address, apeIds.aliceOne) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(interval, params); + + // Alice paid or received nothing: + expect(t0.alice).to.equal(t1.alice); + + // Neither did Carol, who skimmed the preexisting excess balance: + expect(t0.carol).to.equal(t1.carol); + + // The pair kept the fee and the excess, but sent the repayment to Bob: + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(t1.pair).to.be.gte(t1.feeTracker); + + // The skimmable amount covers the entire payment: + const received = t1.bob.sub(t0.bob); + + const paid = received.add(fee); + expect(paid).to.be.lte(enoughShare); + + // Funds either went to Bob or stayed with the pair: + expect(t0.pair.sub(t1.pair)).to.equal(received); + + const leftover = t1.pair.sub(t1.feeTracker); + expect(leftover).to.equal(enoughShare.sub(paid)); + + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + }); + + it("Should work for a large, but repayable, number", async () => { + const fiveYears = 5 * YEAR; + const large: ILoanParams = { + valuation: getBigNumber(1_000_000_000), + annualInterestBPS: 65_535, + expiration: Math.floor(new Date().getTime() / 1000) + 2 * fiveYears, + }; + + await pair.connect(alice).updateLoanParams(apeIds.aliceTwo, large); + + await guineas.transfer(bob.address, large.valuation); + await guineas.transfer(alice.address, MaxUint128); + + // Alice and Bob already had something deposited; this will ensure they + // can pay. Alice's total must not overflow the max BB balance.. + await bentoBox.connect(bob).deposit(guineas.address, bob.address, bob.address, large.valuation, 0); + // (Don't overflow the BentoBox..) + await bentoBox.connect(alice).deposit(guineas.address, alice.address, alice.address, MaxUint128.div(2), 0); + + await pair.connect(bob).lend(apeIds.aliceTwo, large, false); + + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + const inFive = await advanceNextTime(fiveYears); + + await expect(pair.connect(alice).repay(apeIds.aliceTwo, false)) + .to.emit(pair, "LogRepay") + .withArgs(alice.address, apeIds.aliceTwo) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceTwo) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(fiveYears, large); + const linearInterest = valuationShare.mul(large.annualInterestBPS).mul(fiveYears).div(YEAR_BPS); + + const paid = t0.alice.sub(t1.alice); + expect(paid).to.be.gte(valuationShare.add(linearInterest)); + expect(paid).to.be.lte(maxRepayShare); + + // The interest really is ridiculous: + expect(paid).to.be.gte(valuationShare.mul(170_000_000_000_000n)); + + // The difference is rounding errors only, so should be very small: + const difference = maxRepayShare.sub(paid); + expect(difference.mul(1_000_000_000)).to.be.lt(paid); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + // Lower bound makes little sense here.. + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); + }); + + it("Should refuse repayments on expired loans", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + await expect(pair.connect(alice).repay(apeIds.aliceOne, false)).to.be.revertedWith("NFTPair: loan expired"); + }); + + it("Should refuse repayments on nonexistent loans", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + await expect(pair.connect(carol).repay(apeIds.carolOne, false)).to.be.revertedWith("NFTPair: no loan"); + }); + + it("Should refuse to skim too much", async () => { + const interval = 234 * DAY + 5678; + // Does not matter who supplies the payment. Note that there will be + // an excess left; skimming is really only suitable for contracts that + // can calculate the exact repayment needed: + const exactAmount = params.valuation.add(await pair.calculateInterest(params.valuation, interval, params.annualInterestBPS)); + // Round down and subtract some more to be sure, but close: + const notEnoughShare = exactAmount.mul(9).div(20).sub(1337); + + await bentoBox.connect(bob).transfer(guineas.address, bob.address, pair.address, notEnoughShare); + + await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); + await expect(pair.connect(carol).repay(apeIds.aliceOne, true)).to.be.revertedWith("NFTPair: skim too much"); + }); + }); }); diff --git a/utilities/index.ts b/utilities/index.ts index eb540477..2f79ddb9 100644 --- a/utilities/index.ts +++ b/utilities/index.ts @@ -62,3 +62,4 @@ export const setDeploymentSupportedChains = (supportedChains: string[], deployFu } export * from "./time" +export * from "./math" diff --git a/utilities/math.ts b/utilities/math.ts new file mode 100644 index 00000000..19d0ac37 --- /dev/null +++ b/utilities/math.ts @@ -0,0 +1,96 @@ +// Barely enough for an obvious implementation of exp/expm1.. + +import { BigNumber, BigNumberish } from "ethers"; + +type BigRationalish = BigRational | BigNumberish; + +const _guard = {}; + +const _gcd = (m, n) => n == 0 ? m : _gcd(n, m % n); +const gcd = (m, n) => { + const _m = m < 0 ? -m : m; + const _n = n < 0 ? -n : n; + return _m < _n ? _gcd(_n, _m) : _gcd(_m, _n); +}; + +export class BigRational { + n: bigint; + d: bigint; + + constructor(n: bigint, d: bigint, normalize: boolean = true) { + if (normalize) { + const g = gcd(n, d); + const s = (d < 0 ? -1n : 1n); + this.n = s * n / g; + this.d = s * d / g; + } else { + this.n = n; + this.d = d; + } + } + + add(y: BigRationalish) { + const r = BigRational.from(y); + return new BigRational(this.n * r.d + r.n * this.d, this.d * r.d); + } + + mul(y: BigRationalish) { + const r = BigRational.from(y); + return new BigRational(this.n * r.n, this.d * r.d); + } + + div(y: BigRationalish) { + const r = BigRational.from(y); + return new BigRational(this.n * r.d, this.d * r.n); + } + + pow(p: BigNumberish) { + const bp = BigNumber.from(p).toBigInt(); + if (p >= 0) { + return new BigRational(this.n ** bp, this.d ** bp, false); + } else { + const s = this.n < 0 ? -1n : 1n; + return new BigRational(s * this.d ** -bp, s * this.n ** -bp, false); + } + } + + lt(y: BigRationalish) { + const r = BigRational.from(y); + return this.n * r.d < r.n * this.d; + } + + neg() { + return new BigRational(-this.n, this.d, false); + } + + floor(): BigNumber { + return BigNumber.from(this.n / this.d); + } + + ceil(): BigNumber { + return BigNumber.from((this.n + this.d - 1n) / this.d); + } + + static from(x: BigRationalish, y: BigNumberish = 1n) { + return x instanceof BigRational ? x : + new BigRational( + BigNumber.from(x).toBigInt(), + BigNumber.from(y).toBigInt() + ); + } +} + +const factorial = (n: number) => { + let res = 1n; + let c = 1n; + for (let i = 2; i <= n; i++) { res *= ++c; } + return res; +}; + +export function expApprox(x: BigRational, n: number) { + let res = BigRational.from(1); + for (let k = 1; k <= n; k++) { + res = res.add(x.pow(k).div(factorial(k))); + } + return res; +}; From 15e56cce427fe7ee5ffa5ad934edac38b7f49a1c Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 16 Mar 2022 05:38:33 +0100 Subject: [PATCH 047/107] Signed lend/borrow: Include all addresses in signature --- contracts/NFTPair.sol | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index eeb8b6ca..23fe6b08 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -305,11 +305,15 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { return _domainSeparator(); } - // keccak256("Lend(uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant LEND_SIGNATURE_HASH = 0x86c5b2291647820714f2a8238e26e034e2529cf064e0e025f91b989105b586b1; + // NOTE on signature hashes: the domain separator only guarantees that the + // chain ID and master contract are a match, so we explicitly include the + // clone address (and the asset/collateral addresses): - // keccak256("Borrow(uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0x697411e8541f6948a270862fd6b9dfd84c5d5cf563031f2962d2572f8163b155; + // keccak256("Lend(address contract,address collateral,address asset,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x44f4000de30f585f1aaa8bfe6d64c9ecd4b2dba96ea2dc018842bebb898f633e; + + // keccak256("Borrow(address contract,address collateral,address asset,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0x17de252aedd1494927a1b1ce02621620b3303ea5cfa6ed5fd9739342154b9e0d; /// @notice Request and immediately borrow from a pre-committed lender @@ -331,14 +335,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { bytes32 s ) public { require(block.timestamp <= deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[lender]++; bytes32 dataHash = keccak256( abi.encode( LEND_SIGNATURE_HASH, + address(this), + address(collateral), + address(asset), tokenId, params.valuation, params.expiration, params.annualInterestBPS, - nonces[lender]++, + nonce, deadline ) ); @@ -364,14 +372,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { bytes32 s ) public { require(block.timestamp <= deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[borrower]++; bytes32 dataHash = keccak256( abi.encode( BORROW_SIGNATURE_HASH, + address(this), + address(collateral), + address(asset), tokenId, params.valuation, params.expiration, params.annualInterestBPS, - nonces[borrower]++, + nonce, deadline ) ); From 257425f39ba7b9065f89a7814cc81a0c6fb157a4 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 17 Mar 2022 05:24:56 +0100 Subject: [PATCH 048/107] Fix precalculated borrow signature hash --- contracts/NFTPair.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 23fe6b08..432b9a98 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -313,7 +313,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { bytes32 private constant LEND_SIGNATURE_HASH = 0x44f4000de30f585f1aaa8bfe6d64c9ecd4b2dba96ea2dc018842bebb898f633e; // keccak256("Borrow(address contract,address collateral,address asset,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0x17de252aedd1494927a1b1ce02621620b3303ea5cfa6ed5fd9739342154b9e0d; + bytes32 private constant BORROW_SIGNATURE_HASH = 0x2b94e9fcff6aa909e2fc3a768cb30b34e00cdca4a6f2d1a5487bb667b77e7c2f; /// @notice Request and immediately borrow from a pre-committed lender From 138f82b0ce6c5632634cd8f6fad253cabf48b83a Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 20 Mar 2022 06:01:36 +0100 Subject: [PATCH 049/107] Add tests for signed lend/borrow --- test/NFTPair.test.ts | 391 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 387 insertions(+), 4 deletions(-) diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index baa871e6..9fbcad1b 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -3,6 +3,11 @@ import { expect } from "chai"; import { BigNumber, BigNumberish, Contract } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +const { keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack, formatUnits, splitSignature } = ethers.utils; +const { MaxUint256, AddressZero, HashZero } = ethers.constants; +// This one was not defined.. +const MaxUint128 = BigNumber.from(2).pow(128).sub(1); + import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; @@ -34,10 +39,7 @@ interface IPartialLoanParams { annualInterestBPS?: number; } -const { formatUnits } = ethers.utils; -const { MaxUint256, AddressZero, HashZero } = ethers.constants; -// This one was not defined.. -const MaxUint128 = BigNumber.from(2).pow(128).sub(1); +const DOMAIN_SEPARATOR_HASH = keccak256(toUtf8Bytes("EIP712Domain(uint256 chainId,address verifyingContract)")); const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; const nextDecade = Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 10; @@ -794,4 +796,385 @@ describe("NFT Pair", async () => { await expect(pair.connect(carol).repay(apeIds.aliceOne, true)).to.be.revertedWith("NFTPair: skim too much"); }); }); + + describeSnapshot("Signed Lend/Borrow", async () => { + let pair: NFTPair; + let chainId: BigNumberish; + let DOMAIN_SEPARATOR: string; + let BORROW_SIGNATURE_HASH: string; + let LEND_SIGNATURE_HASH: string; + + before(async () => { + pair = await deployPair(); + + chainId = (await ethers.provider.getNetwork()).chainId; + + // TODO: Verify that after a fork this becomes part of the clone + // TODO: Does that matter though? Just use whatever it is? + // TODO: Yes! Need correct asset/coll addresses in the sig also! + DOMAIN_SEPARATOR = keccak256( + defaultAbiCoder.encode(["bytes32", "uint256", "address"], [DOMAIN_SEPARATOR_HASH, chainId, masterContract.address]) + ); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + // TODO: Check whether this interferes? + // for (const id of [apeIds.aliceOne, apeIds.aliceTwo]) { + // await pair.connect(alice).requestLoan(id, params, alice.address, false); + // } + // await pair.connect(bob).lend(apeIds.aliceOne, params, false); + }); + + // Ops happen to have the same method signature other than their name: + const signRequest = async (wallet, op: "Lend" | "Borrow", { tokenId, valuation, expiration, annualInterestBPS, deadline }) => { + const sigTypes = [ + { name: "contract", type: "address" }, + { name: "collateral", type: "address" }, + { name: "asset", type: "address" }, + { name: "tokenId", type: "uint256" }, + { name: "valuation", type: "uint128" }, + { name: "expiration", type: "uint64" }, + { name: "annualInterestBPS", type: "uint16" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ]; + // const sigArgs = sigTypes.map((t) => t.type + " " + t.name); + // const sigHash = keccak256( + // toUtf8Bytes(op + "(" + sigArgs.join(",") + ")") + // ); + + const sigValues = { + contract: pair.address, + collateral: apes.address, + asset: guineas.address, + tokenId, + valuation, + expiration, + annualInterestBPS, + nonce: 0, + deadline, + }; + // const dataHash = keccak256(defaultAbiCoder.encode( + // ["bytes32 sigHash", ...sigArgs], + // Object.values({ sigHash, ...sigValues }) + // )); + // const digest = keccak256( + // solidityPack( + // ["string", "bytes32", "bytes32"], + // ["\x19\x01", DOMAIN_SEPARATOR, dataHash] + // ) + // ); + + // At this point we'd like to sign this digest, but signing arbitrary + // data is made difficult in ethers.js to prevent abuse. So for now we + // use a helper method that basically does everything we just did again: + const sig = await wallet._signTypedData( + // The stuff going into DOMAIN_SEPARATOR: + { chainId, verifyingContract: masterContract.address }, + + // sigHash + { [op]: sigTypes }, + sigValues + ); + return splitSignature(sig); + }; + + it("Should have the expected DOMAIN_SEPARATOR", async () => { + expect(DOMAIN_SEPARATOR).to.equal(await pair.DOMAIN_SEPARATOR()); + }); + + describe("Lend", () => { + // The borrower somehow obtains the signature, then requests and gets the + // loan in one step: + it("Should support pre-approving a loan request", async () => { + // Bob agrees to lend 100 guineas agaist token "carolOne", to be repaid + // no later one year from now. This offer is good for one hour, and can + // be taken up by anyone who can provide the token (and the signature). + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Lend", { + tokenId: apeIds.carolOne, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + // Carol takes the loan: + await expect( + pair + .connect(carol) + .requestAndBorrow( + apeIds.carolOne, + bob.address, + carol.address, + { valuation, expiration, annualInterestBPS }, + false, + deadline, + v, + r, + s + ) + ) + .to.emit(pair, "LogRequestLoan") + .to.emit(pair, "LogLend"); + }); + + it("Should require an exact match on all conditions", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Lend", { + tokenId: apeIds.carolOne, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + // Carol tries to take the loan, but fails because oneo of the + // parameters is different. This pretty much only tests that we do the + // signature check at all, and it feels a bit silly to check every + // variable: if the "success" case passes and any one of these fails, + // then the hash is being checked. + // (Similarly, we could check the token ID, contract, token contracts, + // etc, but we don't, because we know we are hashing those.) + for (const [key, value] of Object.entries(loanParams)) { + const altered = BigNumber.from(value).add(1); + const badLoanParams = { ...loanParams, [key]: altered }; + await expect( + pair.connect(carol).requestAndBorrow(apeIds.carolOne, bob.address, carol.address, badLoanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature invalid"); + } + }); + + it("Should require the lender to be the signer", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Lend", { + tokenId: apeIds.carolOne, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + // Carol tries to take the loan from Alice instead and fails: + await expect( + pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature invalid"); + }); + + it("Should enforce the deadline", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Lend", { + tokenId: apeIds.carolOne, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, deadline, v, r, s] as const; + + // Request fails because the deadline has expired: + await advanceNextTime(3601); + await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.be.revertedWith("NFTPair: signature expired"); + }); + + it("Should not accept the same signature twice", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Lend", { + tokenId: apeIds.carolOne, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, deadline, v, r, s] as const; + + // It works the first time: + await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.emit(pair, "LogLend"); + + // Carol repays the loan to get the token back: + await expect(pair.connect(carol).repay(apeIds.carolOne, false)).to.emit(pair, "LogRepay"); + expect(await apes.ownerOf(apeIds.carolOne)).to.equal(carol.address); + + // It fails now (because the nonce is no longer a match): + await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.be.revertedWith("NFTPair: signature invalid"); + }); + }); + + describe("Borrow", () => { + // Signing a commitment to borrow mainly differs in that: + // - It is not put on chain until the loan is actually made + // - Only the recipient (of the signed message, for now) can lend + // - The borrower can pull out by failing to satisfy the conditions for + // `requestLoan`. + it("Should let borrowers sign a private loan request", async () => { + // Bob commits to borrow 100 guineas and supply token "bobTwo" as + // collateral, to be repaid no later than a year from now. The offer is + // good for one hour, and anyone willing to lend at these terms can + // take it up - if they have the signature. + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Borrow", { + tokenId: apeIds.bobTwo, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + // Alice takes the loan: + await expect( + pair + .connect(alice) + .takeCollateralAndLend(apeIds.bobTwo, bob.address, { valuation, expiration, annualInterestBPS }, false, deadline, v, r, s) + ) + .to.emit(pair, "LogRequestLoan") + .to.emit(pair, "LogLend"); + }); + + it("Should require an exact match on all conditions", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Borrow", { + tokenId: apeIds.bobTwo, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + for (const [key, value] of Object.entries(loanParams)) { + const altered = BigNumber.from(value).add(1); + const badLoanParams = { ...loanParams, [key]: altered }; + await expect( + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, badLoanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature invalid"); + } + }); + + it("Should require the borrower to be the signer", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Borrow", { + tokenId: apeIds.bobTwo, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + // Alice tries to lend to Carol instead and fails: + await expect( + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, carol.address, loanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature invalid"); + }); + + it("Should enforce the deadline", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Borrow", { + tokenId: apeIds.bobTwo, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + + await advanceNextTime(3601); + await expect( + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature expired"); + }); + + it("Should not accept the same signature twice", async () => { + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { r, s, v } = await signRequest(bob, "Borrow", { + tokenId: apeIds.bobTwo, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + const loanParams = { valuation, expiration, annualInterestBPS }; + + await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s)).to.emit( + pair, + "LogLend" + ); + + // Bob repays the loan to get the token back: + await expect(pair.connect(bob).repay(apeIds.bobTwo, false)).to.emit(pair, "LogRepay"); + expect(await apes.ownerOf(apeIds.bobTwo)).to.equal(bob.address); + + // It fails now (because the nonce is no longer a match): + await expect( + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s) + ).to.be.revertedWith("NFTPair: signature invalid"); + }); + }); + + // Not tested, in either case: the loan is set up correctly, both + // collateral and assets change hands, etc. This happens to hold currently, + // but that is only because the implementation is exactly "call + // requestLoan() on behalf of the borrower, then lend() on behalf of the + // lender". + }); }); From 388f48ff8add8263123b425159b1917982228c59 Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Thu, 24 Mar 2022 23:46:26 -0400 Subject: [PATCH 050/107] Changed up protocol fee share --- contracts/NFTPair.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 432b9a98..91bb8074 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -267,15 +267,16 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); // No overflow: at most 128 + 16 bits (fits in BentoBox) - uint256 protocolFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; + uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; + uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS if (skim) { - require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + require(bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), "NFTPair: skim too much"); } else { - bentoBox.transfer(asset, lender, address(this), totalShare); + bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); } // No underflow: follows from OPEN_FEE_BPS <= BPS - uint256 borrowerShare = totalShare - protocolFeeShare; + uint256 borrowerShare = totalShare - openFeeShare; bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); // No overflow: addends (and result) must fit in BentoBox feesEarnedShare += protocolFeeShare; From f71f52a48e039dc2ddb3d91fee60a5c65ed7ad72 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 26 Mar 2022 02:03:59 +0100 Subject: [PATCH 051/107] (Add missing semicolon) --- contracts/NFTPair.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 91bb8074..75d9e167 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -268,7 +268,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); // No overflow: at most 128 + 16 bits (fits in BentoBox) uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; - uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS + uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; if (skim) { require(bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), "NFTPair: skim too much"); From 5cce3c76791a6701f1905e713dc497f37896adc3 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 26 Mar 2022 02:05:46 +0100 Subject: [PATCH 052/107] Rework lend() tests to account for open fee change --- contracts/NFTPair.sol | 5 ++++- test/NFTPair.test.ts | 46 +++++++++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 75d9e167..aa930832 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -271,7 +271,10 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; if (skim) { - require(bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), "NFTPair: skim too much"); + require( + bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), + "NFTPair: skim too much" + ); } else { bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); } diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 9fbcad1b..9b960afd 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -261,18 +261,36 @@ describe("NFT Pair", async () => { await pair.connect(bob).requestLoan(apeIds.bobTwo, params1, carol.address, false); }); + const getShares = ({ valuation }: ILoanParams) => { + const total = valuation.mul(9).div(20); + + // The lender: + // - Lends out the total + // - Receives the open fee + // - Pays the protocol fee (part of the open fee) + // The borrower + // - Receives the total + // - Pays the open fee + // The contract + // - Keeps the protocol fee + const openFee = total.div(100); + const protocolFee = openFee.div(10); + + const borrowerIn = total.sub(openFee); + const lenderOut = total.sub(openFee).add(protocolFee); + return { openFee, protocolFee, borrowerIn, lenderOut }; + }; + it("Should allow anyone to lend", async () => { - const totalShare = params1.valuation.mul(9).div(20); - const openFeeShare = totalShare.div(100); - const borrowShare = totalShare.sub(openFeeShare); + const { lenderOut, borrowerIn } = getShares(params1); await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, false)) .to.emit(pair, "LogLend") .withArgs(carol.address, apeIds.aliceOne) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, carol.address, pair.address, totalShare) + .withArgs(guineas.address, carol.address, pair.address, lenderOut) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, pair.address, alice.address, borrowShare); + .withArgs(guineas.address, pair.address, alice.address, borrowerIn); const loan = await pair.tokenLoan(apeIds.aliceOne); expect(loan.lender).to.equal(carol.address); @@ -281,33 +299,31 @@ describe("NFT Pair", async () => { }); it("Should allow anyone to lend (skim)", async () => { - const totalShare = params1.valuation.mul(9).div(20); + const { lenderOut } = getShares(params1); - await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, totalShare); + await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, lenderOut); await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, true)).to.emit(pair, "LogLend"); }); it("Should revert if skim amount is too low", async () => { - const totalShare = params1.valuation.mul(9).div(20); - const totalShareM1 = totalShare.sub(1); + const { lenderOut } = getShares(params1); + const oneLess = lenderOut.sub(1); - await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, totalShareM1); + await bentoBox.connect(carol).transfer(guineas.address, carol.address, pair.address, oneLess); await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, true)).to.be.revertedWith("NFTPair: skim too much"); }); it("Should allow collateralizing a loan for someone else", async () => { - const totalShare = params1.valuation.mul(9).div(20); - const openFeeShare = totalShare.div(100); - const borrowShare = totalShare.sub(openFeeShare); + const { lenderOut, borrowerIn } = getShares(params1); // Loan was requested by Bob, but money and option to repay go to Carol: await expect(pair.connect(alice).lend(apeIds.bobTwo, params1, false)) .to.emit(pair, "LogLend") .withArgs(alice.address, apeIds.bobTwo) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, alice.address, pair.address, totalShare) + .withArgs(guineas.address, alice.address, pair.address, lenderOut) .to.emit(bentoBox, "LogTransfer") - .withArgs(guineas.address, pair.address, carol.address, borrowShare); + .withArgs(guineas.address, pair.address, carol.address, borrowerIn); const loan = await pair.tokenLoan(apeIds.bobTwo); expect(loan.lender).to.equal(alice.address); From ffeb27d9d3be4bbfc75ddcab4c312ce99175114e Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 26 Mar 2022 02:59:59 +0100 Subject: [PATCH 053/107] Add cook actions for signed methods --- contracts/NFTPair.sol | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index aa930832..eedad991 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -528,6 +528,10 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // Any external call (except to BentoBox) uint8 internal constant ACTION_CALL = 30; + // Signed requests + uint8 internal constant ACTION_REQUEST_AND_BORROW = 40; + uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41; + int256 internal constant USE_VALUE1 = -1; int256 internal constant USE_VALUE2 = -2; @@ -642,6 +646,31 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } else if (returnValues == 2) { (value1, value2) = abi.decode(returnData, (uint256, uint256)); } + } else if (action == ACTION_REQUEST_AND_BORROW) { + ( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32)); + requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, deadline, v, r, s); + } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { + ( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32)); + takeCollateralAndLend(tokenId, borrower, params, skimFunds, deadline, v, r, s); } } } From a50400cb79c669cf710968f32a0fc82eed9e9688 Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Fri, 8 Apr 2022 10:49:19 -0400 Subject: [PATCH 054/107] LendClub changes --- contracts/NFTPair.sol | 53 ++++++++++++++++++++++++++++--------------- hardhat.config.ts | 6 +++++ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index eedad991..b5b7e6e7 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -26,7 +26,19 @@ import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; -import "./interfaces/IERC721.sol"; +import "./interfaces/IERC721.sol"; + + +interface ILendingClub { + // Per token settings. + struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 expiration; // Pay before this or get liquidated + uint16 annualInterestBPS; // Variable cost of taking out the loan + } + function willLend(uint256 tokenId, TokenLoanParams memory params) external returns (bool); + function lendingCondition(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); +} /// @title NFTPair /// @dev This contract allows contract calls to any contract (except BentoBox) @@ -333,28 +345,33 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address recipient, TokenLoanParams memory params, bool skimCollateral, + bool anyTokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public { - require(block.timestamp <= deadline, "NFTPair: signature expired"); - uint256 nonce = nonces[lender]++; - bytes32 dataHash = keccak256( - abi.encode( - LEND_SIGNATURE_HASH, - address(this), - address(collateral), - address(asset), - tokenId, - params.valuation, - params.expiration, - params.annualInterestBPS, - nonce, - deadline - ) - ); - require(ecrecover(_getDigest(dataHash), v, r, s) == lender, "NFTPair: signature invalid"); + if (v == 0 && r == bytes32(0) && s == bytes32(0)) { + require(block.timestamp <= deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[lender]++; + bytes32 dataHash = keccak256( + abi.encode( + LEND_SIGNATURE_HASH, + address(this), + anyTokenId ? 0 : tokenId, + params.valuation, + params.expiration, + params.annualInterestBPS, + anyTokenId, + nonce, + deadline + ) + ); + require(ecrecover(_getDigest(dataHash), v, r, s) == lender, "NFTPair: signature invalid"); + } else { + require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you."); + } + _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); _lend(lender, tokenId, params, false); } diff --git a/hardhat.config.ts b/hardhat.config.ts index bead605a..a8db04a7 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -238,6 +238,12 @@ const config: HardhatUserConfig = { }, { version: "0.8.10", + settings: { + optimizer: { + enabled: true, + runs: 690, + }, + }, }, { version: "0.7.6", From abb0ba6818258e69c083bbd9df3c3af08907d727 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 26 Mar 2022 05:06:38 +0100 Subject: [PATCH 055/107] Improve test coverage to all but cooks; start cook tests --- .solcover.js | 23 ++-- test/NFTPair.test.ts | 310 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 303 insertions(+), 30 deletions(-) diff --git a/.solcover.js b/.solcover.js index 5c8866f8..1f27769d 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,13 +1,10 @@ -{ - // We are always skipping mocks and interfaces, add specific files here - skipFiles: [ - "libraries/FixedPoint.sol", - "libraries/FullMath.sol", - "libraries/SignedSafeMath.sol", - "flat/BentoBoxFlat.sol", - "flat/KashiPairFlat.sol", - "flat/SushiSwapSwapperFlat.sol", - "mocks/", - "interfaces/" - ], -} \ No newline at end of file +module.exports = { + skipFiles: [ + "libraries/FixedPoint.sol", + "libraries/FullMath.sol", + "libraries/SignedSafeMath.sol", + "BentoBoxFlat.sol", + "mocks/", + "interfaces/" + ] +} diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 9b960afd..5f7801a4 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -8,10 +8,11 @@ const { MaxUint256, AddressZero, HashZero } = ethers.constants; // This one was not defined.. const MaxUint128 = BigNumber.from(2).pow(128).sub(1); +const hashUtf8String = (s: string) => keccak256(toUtf8Bytes(s)); + import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; -import { Cook, encodeLoanParamsNFT } from "./PrivatePool"; const LoanStatus = { INITIAL: 0, @@ -19,6 +20,29 @@ const LoanStatus = { OUTSTANDING: 2, }; +// Cook actions +const ACTION_REPAY = 2; +const ACTION_REMOVE_COLLATERAL = 4; + +const ACTION_REQUEST_LOAN = 12; + +// Function on BentoBox +const ACTION_BENTO_DEPOSIT = 20; +const ACTION_BENTO_WITHDRAW = 21; +const ACTION_BENTO_TRANSFER = 22; +const ACTION_BENTO_TRANSFER_MULTIPLE = 23; +const ACTION_BENTO_SETAPPROVAL = 24; + +// Any external call (except to BentoBox) +const ACTION_CALL = 30; + +// Signed requests +const ACTION_REQUEST_AND_BORROW = 40; +const ACTION_TAKE_COLLATERAL_AND_LEND = 41; + +const USE_VALUE1 = -1; +const USE_VALUE2 = -2; + interface IDeployParams { collateral: string; asset: string; @@ -39,10 +63,12 @@ interface IPartialLoanParams { annualInterestBPS?: number; } -const DOMAIN_SEPARATOR_HASH = keccak256(toUtf8Bytes("EIP712Domain(uint256 chainId,address verifyingContract)")); +const DOMAIN_SEPARATOR_HASH = hashUtf8String("EIP712Domain(uint256 chainId,address verifyingContract)"); -const nextYear = Math.floor(new Date().getTime() / 1000) + 86400 * 365; -const nextDecade = Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 10; +const DAY = 24 * 3600; +const YEAR = 365 * DAY; +const nextYear = Math.floor(new Date().getTime() / 1000) + YEAR; +const nextDecade = Math.floor(new Date().getTime() / 1000) + YEAR * 10; describe("NFT Pair", async () => { let apes: ERC721Mock; @@ -538,13 +564,18 @@ describe("NFT Pair", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration + 1_000_000]); await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); }); + + it("Should let anyone withdraw stray NFT tokens", async () => { + await apes.connect(bob).transferFrom(bob.address, pair.address, apeIds.bobOne); + await expect(pair.connect(bob).removeCollateral(apeIds.bobOne, bob.address)) + .to.emit(pair, "LogRemoveCollateral") + .withArgs(apeIds.bobOne, bob.address); + }); }); describeSnapshot("Repay", () => { let pair: NFTPair; - const DAY = 24 * 3600; - const YEAR = 365 * DAY; const params: ILoanParams = { valuation: getBigNumber(1), annualInterestBPS: 10_000, @@ -813,7 +844,7 @@ describe("NFT Pair", async () => { }); }); - describeSnapshot("Signed Lend/Borrow", async () => { + describeSnapshot("Signed Lend/Borrow", () => { let pair: NFTPair; let chainId: BigNumberish; let DOMAIN_SEPARATOR: string; @@ -825,9 +856,6 @@ describe("NFT Pair", async () => { chainId = (await ethers.provider.getNetwork()).chainId; - // TODO: Verify that after a fork this becomes part of the clone - // TODO: Does that matter though? Just use whatever it is? - // TODO: Yes! Need correct asset/coll addresses in the sig also! DOMAIN_SEPARATOR = keccak256( defaultAbiCoder.encode(["bytes32", "uint256", "address"], [DOMAIN_SEPARATOR_HASH, chainId, masterContract.address]) ); @@ -835,12 +863,6 @@ describe("NFT Pair", async () => { for (const signer of [deployer, alice, bob, carol]) { await apes.connect(signer).setApprovalForAll(pair.address, true); } - - // TODO: Check whether this interferes? - // for (const id of [apeIds.aliceOne, apeIds.aliceTwo]) { - // await pair.connect(alice).requestLoan(id, params, alice.address, false); - // } - // await pair.connect(bob).lend(apeIds.aliceOne, params, false); }); // Ops happen to have the same method signature other than their name: @@ -914,7 +936,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Lend", { + const { v, r, s } = await signRequest(bob, "Lend", { tokenId: apeIds.carolOne, valuation, expiration, @@ -1193,4 +1215,258 @@ describe("NFT Pair", async () => { // requestLoan() on behalf of the borrower, then lend() on behalf of the // lender". }); + + describeSnapshot("Withdraw Fees", () => { + let pair: NFTPair; + + const params: ILoanParams = { + valuation: getBigNumber(3), + annualInterestBPS: 5_000, + expiration: Math.floor(new Date().getTime() / 1000) + YEAR, + }; + const valuationShare = params.valuation.mul(9).div(20); + const borrowerShare = valuationShare.mul(99).div(100); + + // Theoretically this could fail to actually bound the repay share because + // of the FP math used. Double check using a more exact method if that + // happens: + const YEAR_BPS = YEAR * 10_000; + const COMPOUND_TERMS = 6; + const getMaxRepayShare = (time, params_) => { + // We mimic what the contract does, but without rounding errors in the + // approximation, so the upper bound should be strict. + // 1. Calculate exact amount owed; round it down, like the contract does. + // 2. Convert that to Bento shares (still hardcoded at 9/20); rounding up + const x = BigRational.from(time * params_.annualInterestBPS).div(YEAR_BPS); + return expApprox(x, COMPOUND_TERMS).mul(params_.valuation).floor().mul(9).add(19).div(20); + }; + + before(async () => { + pair = await deployPair(); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + await pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false); + await pair.connect(bob).lend(apeIds.aliceOne, params, false); + + expect(await pair.feeTo()).to.equal(AddressZero); + expect(await masterContract.feeTo()).to.equal(AddressZero); + expect(await pair.owner()).to.equal(AddressZero); + expect(await masterContract.owner()).to.equal(deployer.address); + }); + + // Scenario covered by BentoBox (refuses to send to zero address) + it("Should not burn funds if feeTo not set", async () => { + await expect(pair.connect(alice).withdrawFees()).to.be.revertedWith("BentoBox: to not set"); + }); + + it("Should let only the deployer change the fee recipient", async () => { + await expect(masterContract.connect(bob).setFeeTo(alice.address)).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect(masterContract.connect(deployer).setFeeTo(alice.address)).to.emit(masterContract, "LogFeeTo").withArgs(alice.address); + + expect(await masterContract.feeTo()).to.equal(alice.address); + expect(await pair.feeTo()).to.equal(AddressZero); + }); + + it("Should let anyone request a withdrawal - to the operator", async () => { + await masterContract.connect(deployer).setFeeTo(carol.address); + // 10% of the 1% open fee on the loan: + const feeShare = params.valuation.div(1000).mul(9).div(20); + expect(feeShare).to.be.gt(0); + expect(await pair.feesEarnedShare()).to.equal(feeShare); + await expect(pair.connect(bob).withdrawFees()) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, carol.address, feeShare) + .to.emit(pair, "LogWithdrawFees") + .withArgs(carol.address, feeShare); + + expect(await pair.feesEarnedShare()).to.equal(0); + + await expect(pair.connect(bob).withdrawFees()).to.emit(pair, "LogWithdrawFees").withArgs(carol.address, 0); + }); + }); + + describeSnapshot("Edge Cases", () => { + // For coverage mostly - entire scenario not really necessary: + let pair: NFTPair; + + before(async () => { + pair = await deployPair(); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + }); + + it("Should revert if payable interest exceeds 2^128", async () => { + await expect(pair.calculateInterest(MaxUint128, YEAR, 10_000)).to.be.reverted; + }); + }); + + describeSnapshot("Cook Scenarios", () => { + // Uses its own + let pair: NFTPair; + let chainId: BigNumberish; + let DOMAIN_SEPARATOR: string; + let BORROW_SIGNATURE_HASH: string; + let LEND_SIGNATURE_HASH: string; + + const tokenIds: BigNumber[] = []; + + const approveHash = hashUtf8String("Give FULL access to funds in (and approved to) BentoBox?"); + const revokeHash = hashUtf8String("Revoke access to BentoBox?"); + const signBentoApprovalRequest = async (wallet, approved: boolean, nonce?: number) => { + const sigTypes = [ + { name: "warning", type: "string" }, + { name: "user", type: "address" }, + { name: "contract", type: "address" }, + { name: "approved", type: "bool" }, + { name: "nonce", type: "uint256" }, + ]; + + const sigValues = { + warning: approved ? approveHash : revokeHash, + user: wallet.address, + contract: bentoBox.address, + approved, + nonce: nonce ?? 0, + }; + + // At this point we'd like to sign this digest, but signing arbitrary + // data is made difficult in ethers.js to prevent abuse. So for now we + // use a helper method that basically does everything we just did again: + const sig = await wallet._signTypedData( + // The stuff going into DOMAIN_SEPARATOR: + { name: hashUtf8String("BentoBox V1"), chainId, verifyingContract: bentoBox.address }, + + // sigHash + { SetMasterContractApproval: sigTypes }, + sigValues + ); + return splitSignature(sig); + }; + + before(async () => { + chainId = (await ethers.provider.getNetwork()).chainId; + pair = await deployPair(); + // Undo some of the setup, so we start from scratch: + const mc = masterContract.address; + const hz = HashZero; + for (const signer of [alice, bob, carol]) { + const addr = signer.address; + const bb = bentoBox.connect(signer); + await bb.setMasterContractApproval(addr, mc, false, 0, hz, hz); + + await guineas.transfer(addr, getBigNumber(10_000)); + await guineas.connect(signer).approve(bentoBox.address, MaxUint256); + + // We added profit in the setup; 3000 guineas became 3000 shares + await bb.withdraw(guineas.address, addr, addr, 0, getBigNumber(3000)); + } + expect(await bentoBox.balanceOf(guineas.address, alice.address)).to.equal(0); + + // (No permit equivalent that we could do via a cook..) + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + for (let i = 0; i < 10; i++) { + tokenIds.push(await mintApe(bob.address)); + } + }); + + const requestLoans = (getArgs: (i: number) => [ILoanParams, string, boolean]) => { + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + for (let i = 0; i < tokenIds.length; i++) { + const tokenId = tokenIds[i]; + const [params, recipient, skim] = getArgs(i); + actions.push(ACTION_REQUEST_LOAN); + values.push(0); + datas.push( + encodeParameters( + ["uint256", "tuple(uint128 valuation, uint64 expiration, uint16 annualInterestBPS)", "address", "bool"], + [tokenId, params, recipient, skim] + ) + ); + } + return pair.connect(bob).cook(actions, values, datas); + }; + + // Suppose this is one use case.. + it("Should allow requesting multiple loans", async () => { + // All apes come from Bob: + await expect( + requestLoans((i) => [ + { + valuation: getBigNumber((i + 1) * 12), + expiration: nextYear, + annualInterestBPS: i * 500, + }, + [alice.address, bob.address, carol.address][i % 3], + false, + ]) + ) + .to.emit(apes, "Transfer") + .withArgs(bob.address, pair.address, tokenIds[0]) + .to.emit(apes, "Transfer") + .withArgs(bob.address, pair.address, tokenIds[1]) + .to.emit(apes, "Transfer") + .withArgs(bob.address, pair.address, tokenIds[9]) + .to.emit(pair, "LogRequestLoan") + .withArgs(alice.address, tokenIds[6], getBigNumber(7 * 12), nextYear, 6 * 500) + .to.emit(pair, "LogRequestLoan") + .withArgs(bob.address, tokenIds[7], getBigNumber(8 * 12), nextYear, 7 * 500) + .to.emit(pair, "LogRequestLoan") + .withArgs(carol.address, tokenIds[8], getBigNumber(9 * 12), nextYear, 8 * 500); + }); + + it("Should have the expected domain separator", async () => { + // Note the string type for "name", while in reality we hash it, + // resulting in a "bytes32". This type of thing causes problems if we use + // the ethers library for signing structured messages. + const hash = hashUtf8String("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); + const domainSeparator = keccak256( + defaultAbiCoder.encode(["bytes32", "bytes32", "uint256", "address"], [hash, hashUtf8String("BentoBox V1"), chainId, bentoBox.address]) + ); + expect(domainSeparator).to.equal(await bentoBox.DOMAIN_SEPARATOR()); + }); + + // Failing the approval request; types in sig not an exact match, so have to + // sign the message without help from ethers' abstractions. See also the + // DOMAIN_SEPARATOR test; suspect a similar issue. + // it("Should handle depositing and lending with minimal setup", async () => { + // await requestLoans((i) => [ + // { + // valuation: getBigNumber((i + 1) * 12), + // expiration: nextYear, + // annualInterestBPS: i * 500, + // }, + // [alice.address, bob.address, carol.address][i % 3], + // false, + // ]); + + // const actions: number[] = []; + // const values: any[] = []; + // const datas: any[] = []; + + // // 1. Set contract approval (can check if needed) + // const { v, r, s } = await signBentoApprovalRequest(alice, true); + // actions.push(ACTION_BENTO_SETAPPROVAL); + // values.push(0); + // datas.push(encodeParameters( + // ["address", "address", "bool", "uint8", "bytes32", "bytes32"], + // [alice.address, masterContract.address, true, v, r, s] + // )); + + // // 2. Bento permit + // // 3. Bento deposit + // // 4. Lend + // await pair.connect(alice).cook(actions, values, datas); + // }); + }); }); From 5bf66992b907245c0d19e46a5c2365b1b6f62c63 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 11 Apr 2022 04:13:58 +0200 Subject: [PATCH 056/107] Disallow calls to the collateral contract --- contracts/NFTPair.sol | 4 ++-- test/NFTPair.test.ts | 39 +++++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index eedad991..b6a6a6f0 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -568,7 +568,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. - /// Calls to `bentoBox` are not allowed for obvious security reasons. + /// Calls to `bentoBox` or `collateral` are not allowed for security reasons. /// This also means that calls made from this contract shall *not* be trusted. function _call( uint256 value, @@ -589,7 +589,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { callData = abi.encodePacked(callData, value1, value2); } - require(callee != address(bentoBox) && callee != address(this), "NFTPair: can't call"); + require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), "NFTPair: can't call"); (bool success, bytes memory returnData) = callee.call{value: value}(callData); require(success, "NFTPair: call failed"); diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 5f7801a4..bae3ea10 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -119,11 +119,13 @@ describe("NFT Pair", async () => { }); // Specific to the mock implementation.. - const mintApe = async (ownerAddress) => { - const id = await apes.totalSupply(); - await apes.mint(ownerAddress); + // TODO: Upgrade BoringSolidity to version that returns the ID: + const mintToken = async (mockContract, ownerAddress) => { + const id = await mockContract.totalSupply(); + await mockContract.mint(ownerAddress); return id; }; + const mintApe = (ownerAddress) => mintToken(apes, ownerAddress); before(async () => { const weth = await deployContract("WETH9Mock"); @@ -1340,7 +1342,11 @@ describe("NFT Pair", async () => { // use a helper method that basically does everything we just did again: const sig = await wallet._signTypedData( // The stuff going into DOMAIN_SEPARATOR: - { name: hashUtf8String("BentoBox V1"), chainId, verifyingContract: bentoBox.address }, + { + name: hashUtf8String("BentoBox V1"), + chainId, + verifyingContract: bentoBox.address, + }, // sigHash { SetMasterContractApproval: sigTypes }, @@ -1436,6 +1442,31 @@ describe("NFT Pair", async () => { expect(domainSeparator).to.equal(await bentoBox.DOMAIN_SEPARATOR()); }); + it("Should disallow taking collateral NFTs via ACTION_CALL", async () => { + // To supply collateral, we have to approve the NFT pair to spend our + // tokens. If we also allow arbitrary calls to the collateral token, then + // this can be used to steal NFTs: + const takeNftFrom = (contract, owner, tokenId) => { + const params = encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [contract.address, contract.interface.encodeFunctionData("transferFrom", [owner.address, bob.address, tokenId]), false, false, 0] + ); + return pair.connect(bob).cook([ACTION_CALL], [0], [params]); + }; + await expect(takeNftFrom(apes, alice, apeIds.aliceOne)).to.be.revertedWith("NFTPair: can't call"); + + // As an extra check, the same call for some other token works - if an + // owner has ill-advisedly allowed the pair to spend that other token: + const bears = await deployContract("ERC721Mock"); + const carolBearId = await mintToken(bears, carol.address); + // WIthout approval: + await expect(takeNftFrom(bears, carol, carolBearId)).to.be.revertedWith("NFTPair: call failed"); + + // With approval: + await bears.connect(carol).setApprovalForAll(pair.address, true); + await expect(takeNftFrom(bears, carol, carolBearId)).to.emit(bears, "Transfer").withArgs(carol.address, bob.address, carolBearId); + }); + // Failing the approval request; types in sig not an exact match, so have to // sign the message without help from ethers' abstractions. See also the // DOMAIN_SEPARATOR test; suspect a similar issue. From 8474d9a0ba501d0de5519e1b8f151cca889c94e7 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 11 Apr 2022 04:45:26 +0200 Subject: [PATCH 057/107] Rework lend/borrow signatures; add "any token" option --- contracts/NFTPair.sol | 22 ++++---- test/NFTPair.test.ts | 117 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 106 insertions(+), 33 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index b6a6a6f0..3481a21f 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -313,11 +313,11 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // chain ID and master contract are a match, so we explicitly include the // clone address (and the asset/collateral addresses): - // keccak256("Lend(address contract,address collateral,address asset,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant LEND_SIGNATURE_HASH = 0x44f4000de30f585f1aaa8bfe6d64c9ecd4b2dba96ea2dc018842bebb898f633e; + // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x9bcf99059e3c2bf4522b1950b84c7777535a4a54075afadd793dc5f00a5e7aa9; - // keccak256("Borrow(address contract,address collateral,address asset,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0x2b94e9fcff6aa909e2fc3a768cb30b34e00cdca4a6f2d1a5487bb667b77e7c2f; + // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0x2a060161383de688deddeac5b5c10a5ded88b7f78db776edf545b1e427997c09; /// @notice Request and immediately borrow from a pre-committed lender @@ -327,12 +327,14 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @param recipient Address to receive the loan. /// @param params Loan parameters requested, and signed by the lender /// @param skimCollateral True if the collateral has already been transfered + /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. function requestAndBorrow( uint256 tokenId, address lender, address recipient, TokenLoanParams memory params, bool skimCollateral, + bool anyTokenId, uint256 deadline, uint8 v, bytes32 r, @@ -344,9 +346,8 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { abi.encode( LEND_SIGNATURE_HASH, address(this), - address(collateral), - address(asset), - tokenId, + anyTokenId ? 0 : tokenId, + anyTokenId, params.valuation, params.expiration, params.annualInterestBPS, @@ -381,8 +382,6 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { abi.encode( BORROW_SIGNATURE_HASH, address(this), - address(collateral), - address(asset), tokenId, params.valuation, params.expiration, @@ -653,12 +652,13 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address recipient, TokenLoanParams memory params, bool skimCollateral, + bool anyTokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s - ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32)); - requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, deadline, v, r, s); + ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, uint256, uint8, bytes32, bytes32)); + requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, deadline, v, r, s); } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { ( uint256 tokenId, diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index bae3ea10..39805fd5 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -867,12 +867,9 @@ describe("NFT Pair", async () => { } }); - // Ops happen to have the same method signature other than their name: - const signRequest = async (wallet, op: "Lend" | "Borrow", { tokenId, valuation, expiration, annualInterestBPS, deadline }) => { + const signBorrowRequest = async (wallet, { tokenId, valuation, expiration, annualInterestBPS, deadline }) => { const sigTypes = [ { name: "contract", type: "address" }, - { name: "collateral", type: "address" }, - { name: "asset", type: "address" }, { name: "tokenId", type: "uint256" }, { name: "valuation", type: "uint128" }, { name: "expiration", type: "uint64" }, @@ -882,13 +879,11 @@ describe("NFT Pair", async () => { ]; // const sigArgs = sigTypes.map((t) => t.type + " " + t.name); // const sigHash = keccak256( - // toUtf8Bytes(op + "(" + sigArgs.join(",") + ")") + // toUtf8Bytes("Borrow(" + sigArgs.join(",") + ")") // ); const sigValues = { contract: pair.address, - collateral: apes.address, - asset: guineas.address, tokenId, valuation, expiration, @@ -915,7 +910,39 @@ describe("NFT Pair", async () => { { chainId, verifyingContract: masterContract.address }, // sigHash - { [op]: sigTypes }, + { Borrow: sigTypes }, + sigValues + ); + return splitSignature(sig); + }; + + const signLendRequest = async (wallet, { tokenId, anyTokenId, valuation, expiration, annualInterestBPS, deadline }) => { + const sigTypes = [ + { name: "contract", type: "address" }, + { name: "tokenId", type: "uint256" }, + { name: "anyTokenId", type: "bool" }, + { name: "valuation", type: "uint128" }, + { name: "expiration", type: "uint64" }, + { name: "annualInterestBPS", type: "uint16" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ]; + const sigValues = { + contract: pair.address, + tokenId, + anyTokenId, + valuation, + expiration, + annualInterestBPS, + nonce: 0, + deadline, + }; + const sig = await wallet._signTypedData( + // The stuff going into DOMAIN_SEPARATOR: + { chainId, verifyingContract: masterContract.address }, + + // sigHash + { Lend: sigTypes }, sigValues ); return splitSignature(sig); @@ -938,8 +965,49 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { v, r, s } = await signRequest(bob, "Lend", { + const { v, r, s } = await signLendRequest(bob, { tokenId: apeIds.carolOne, + anyTokenId: false, + valuation, + expiration, + annualInterestBPS, + deadline, + }); + + // Carol takes the loan: + await expect( + pair + .connect(carol) + .requestAndBorrow( + apeIds.carolOne, + bob.address, + carol.address, + { valuation, expiration, annualInterestBPS }, + false, + false, + deadline, + v, + r, + s + ) + ) + .to.emit(pair, "LogRequestLoan") + .to.emit(pair, "LogLend"); + }); + + it("Should support pre-approving a loan request for any token", async () => { + // Bob agrees to lend 100 guineas agaist any ape, to be repaid + // no later one year from now. This offer is good for one hour, and can + // be taken up by anyone who can provide the token (and the signature). + const { timestamp } = await ethers.provider.getBlock("latest"); + const valuation = getBigNumber(100); + const expiration = timestamp + 365 * 24 * 3600; + const annualInterestBPS = 15000; + const deadline = timestamp + 3600; + + const { v, r, s } = await signLendRequest(bob, { + tokenId: 0, + anyTokenId: true, valuation, expiration, annualInterestBPS, @@ -956,6 +1024,7 @@ describe("NFT Pair", async () => { carol.address, { valuation, expiration, annualInterestBPS }, false, + true, deadline, v, r, @@ -973,8 +1042,9 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Lend", { + const { r, s, v } = await signLendRequest(bob, { tokenId: apeIds.carolOne, + anyTokenId: false, valuation, expiration, annualInterestBPS, @@ -993,7 +1063,7 @@ describe("NFT Pair", async () => { const altered = BigNumber.from(value).add(1); const badLoanParams = { ...loanParams, [key]: altered }; await expect( - pair.connect(carol).requestAndBorrow(apeIds.carolOne, bob.address, carol.address, badLoanParams, false, deadline, v, r, s) + pair.connect(carol).requestAndBorrow(apeIds.carolOne, bob.address, carol.address, badLoanParams, false, false, deadline, v, r, s) ).to.be.revertedWith("NFTPair: signature invalid"); } }); @@ -1005,8 +1075,9 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Lend", { + const { r, s, v } = await signLendRequest(bob, { tokenId: apeIds.carolOne, + anyTokenId: false, valuation, expiration, annualInterestBPS, @@ -1016,7 +1087,7 @@ describe("NFT Pair", async () => { const loanParams = { valuation, expiration, annualInterestBPS }; // Carol tries to take the loan from Alice instead and fails: await expect( - pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, deadline, v, r, s) + pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, false, deadline, v, r, s) ).to.be.revertedWith("NFTPair: signature invalid"); }); @@ -1027,8 +1098,9 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Lend", { + const { r, s, v } = await signLendRequest(bob, { tokenId: apeIds.carolOne, + anyTokenId: false, valuation, expiration, annualInterestBPS, @@ -1036,7 +1108,7 @@ describe("NFT Pair", async () => { }); const loanParams = { valuation, expiration, annualInterestBPS }; - const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, deadline, v, r, s] as const; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; // Request fails because the deadline has expired: await advanceNextTime(3601); @@ -1050,8 +1122,9 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Lend", { + const { r, s, v } = await signLendRequest(bob, { tokenId: apeIds.carolOne, + anyTokenId: false, valuation, expiration, annualInterestBPS, @@ -1059,7 +1132,7 @@ describe("NFT Pair", async () => { }); const loanParams = { valuation, expiration, annualInterestBPS }; - const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, deadline, v, r, s] as const; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; // It works the first time: await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.emit(pair, "LogLend"); @@ -1090,7 +1163,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Borrow", { + const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, expiration, @@ -1115,7 +1188,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Borrow", { + const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, expiration, @@ -1140,7 +1213,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Borrow", { + const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, expiration, @@ -1162,7 +1235,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Borrow", { + const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, expiration, @@ -1185,7 +1258,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signRequest(bob, "Borrow", { + const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, expiration, From 8828d482e0b16467b6aad14c25ffbbe21449f2e8 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 13 Apr 2022 18:18:20 +0200 Subject: [PATCH 058/107] Slightly change LendingClub signature and add test case --- contracts/NFTPair.sol | 18 +++++- contracts/mocks/LendingClubMock.sol | 94 +++++++++++++++++++++++++++++ test/NFTPair.test.ts | 64 +++++++++++++++++++- 3 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 contracts/mocks/LendingClubMock.sol diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 82596bbc..0a970358 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -36,9 +36,21 @@ struct TokenLoanParams { interface ILendingClub { // Per token settings. - function willLend(uint256 tokenId, TokenLoanParams memory params) external returns (bool); + function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); - function lendingCondition(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); + function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); +} + +interface INFTPair { + function collateral() external view returns (IERC721); + + function asset() external view returns (IERC20); + + function masterContract() external view returns (address); + + function bentoBox() external view returns (IBentoBoxV1); + + function removeCollateral(uint256 tokenId, address to) external; } /// @title NFTPair @@ -349,7 +361,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { bytes32 s ) public { if (v == 0 && r == bytes32(0) && s == bytes32(0)) { - require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you."); + require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); } else { require(block.timestamp <= deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol new file mode 100644 index 00000000..a8ec1840 --- /dev/null +++ b/contracts/mocks/LendingClubMock.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "../NFTPair.sol"; + +// Minimal implementation to set up some tests. +contract LendingClubMock { + INFTPair private immutable nftPair; + address private immutable investor; + + constructor(INFTPair _nftPair, address _investor) public { + nftPair = _nftPair; + investor = _investor; + } + + function init() public { + nftPair.bentoBox().setMasterContractApproval( + address(this), + address(nftPair.masterContract()), + true, + 0, + bytes32(0), + bytes32(0) + ); + } + + function willLend(uint256 tokenId, TokenLoanParams memory requested) + external + view + returns (bool) + { + if (msg.sender != address(nftPair)) { + return false; + } + TokenLoanParams memory accepted = _lendingConditions(tokenId); + // Valuation has to be an exact match, everything else must be at least + // as good for the lender as `accepted`. + + return + requested.valuation == accepted.valuation && + requested.expiration <= accepted.expiration && + requested.annualInterestBPS >= accepted.annualInterestBPS; + } + + function _lendingConditions(uint256 tokenId) + private + view + returns (TokenLoanParams memory) + { + TokenLoanParams memory conditions; + // No specific conditions given, but we'll take all even-numbered + // ones at 100% APY: + if (tokenId % 2 == 0) { + // 256-bit addition fits by the above check. + // Cast is.. relatively safe: this is a mock implementation, + // production use is unlikely to follow this pattern for valuing + // loans, and manipulating the token ID can only break the logic by + // making the loan "safer" for the lender. + conditions.valuation = uint128((tokenId + 1) * 10**18); + // Addition and cast are safe for the foreseeable future. + conditions.expiration = uint64(block.timestamp + 365 days); + conditions.annualInterestBPS = 10_000; + } + return conditions; + } + + function lendingConditions(address _nftPair, uint256 tokenId) + external + view + returns (TokenLoanParams memory) + { + if (_nftPair != address(nftPair)) { + TokenLoanParams memory empty; + return empty; + } else { + return _lendingConditions(tokenId); + } + } + + function seizeCollateral(uint256 tokenId) external { + nftPair.removeCollateral(tokenId, investor); + } + + function withdrawFunds(uint256 bentoShares) external { + nftPair.bentoBox().transfer( + nftPair.asset(), + address(this), + investor, + bentoShares + ); + } +} diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 39805fd5..e77177d1 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -11,7 +11,7 @@ const MaxUint128 = BigNumber.from(2).pow(128).sub(1); const hashUtf8String = (s: string) => keccak256(toUtf8Bytes(s)); import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; -import { BentoBoxMock, ERC20Mock, ERC721Mock, WETH9Mock, NFTPair } from "../typechain"; +import { BentoBoxMock, ERC20Mock, ERC721Mock, LendingClubMock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; const LoanStatus = { @@ -1573,4 +1573,66 @@ describe("NFT Pair", async () => { // await pair.connect(alice).cook(actions, values, datas); // }); }); + + describeSnapshot("Lending Club", () => { + let pair: NFTPair; + let lendingClub: LendingClubMock; + let emptyLendingClub: LendingClubMock; + + const nextWeek = Math.floor(new Date().getTime() / 1000) + 86400 * 7; + + before(async () => { + pair = await deployPair(); + lendingClub = await deployContract("LendingClubMock", pair.address, bob.address); + await lendingClub.init(); + emptyLendingClub = await deployContract("LendingClubMock", AddressZero, AddressZero); + + for (const signer of [alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + // Bob deposits 10 guineas into the lending "club" that he is the sole + // investor of. + await bentoBox.connect(bob).deposit(guineas.address, bob.address, lendingClub.address, getBigNumber(10), 0); + }); + + const borrow = (borrower: SignerWithAddress, club: LendingClubMock, tokenId: BigNumberish, params: ILoanParams) => + pair.connect(borrower).requestAndBorrow(tokenId, club.address, borrower.address, params, false, false, 0, 0, HashZero, HashZero); + + it("Should allow LendingClubs to approve or reject loans", async () => { + // Mock implementation detail: tokenId has to be even + expect(getBigNumber(apeIds.aliceOne, 0).mod(2)).to.equal(0); + + const valuation = getBigNumber(1).add(apeIds.aliceOne); + const expiration = nextWeek; + const annualInterestBPS = 20_000; + + await expect( + borrow(alice, emptyLendingClub, apeIds.aliceOne, { + valuation, + expiration, + annualInterestBPS, + }) + ).to.be.revertedWith("NFTPair: LendingClub does not like you"); + + await expect( + borrow(alice, lendingClub, apeIds.aliceOne, { + valuation, + expiration, + annualInterestBPS, + }) + ) + .to.emit(pair, "LogRequestLoan") + .to.emit(pair, "LogLend"); + + expect(getBigNumber(apeIds.aliceTwo, 0).mod(2)).to.equal(1); + await expect( + borrow(alice, lendingClub, apeIds.aliceTwo, { + valuation, + expiration, + annualInterestBPS, + }) + ).to.be.revertedWith("NFTPair: LendingClub does not like you"); + }); + }); }); From 208e61f9be299b863c94c5101b5e61faffe3ee77 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 14 Apr 2022 04:02:52 +0200 Subject: [PATCH 059/107] (Address warning on mock contract) --- contracts/mocks/LendingClubMock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index a8ec1840..3abc5f10 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -46,7 +46,7 @@ contract LendingClubMock { function _lendingConditions(uint256 tokenId) private - view + pure returns (TokenLoanParams memory) { TokenLoanParams memory conditions; From a75094dce19eb4d5f39c06b647add35423b431cc Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 14 Apr 2022 04:41:55 +0200 Subject: [PATCH 060/107] Take duration instead of expiration as loan parameter --- contracts/NFTPair.sol | 40 ++++--- contracts/mocks/LendingClubMock.sol | 5 +- test/NFTPair.test.ts | 166 ++++++++++++++-------------- 3 files changed, 110 insertions(+), 101 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 0a970358..35d08f46 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -30,7 +30,7 @@ import "./interfaces/IERC721.sol"; struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 expiration; // Pay before this or get liquidated + uint64 duration; // Length of loan in seconds uint16 annualInterestBPS; // Variable cost of taking out the loan } @@ -62,8 +62,8 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 expiration, uint16 annualInterestBPS); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 expiration, uint16 annualInterestBPS); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request @@ -186,9 +186,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( - params.expiration >= cur.expiration && - params.valuation <= cur.valuation && - params.annualInterestBPS <= cur.annualInterestBPS, + params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -201,7 +199,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.expiration, params.annualInterestBPS); + emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); } function _requestLoan( @@ -225,7 +223,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { tokenLoan[tokenId] = loan; tokenLoanParams[tokenId] = params; - emit LogRequestLoan(to, tokenId, params.valuation, params.expiration, params.annualInterestBPS); + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); } /// @notice Deposit an NFT as collateral and request a loan against it @@ -255,7 +253,11 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: require(msg.sender == loan.lender, "NFTPair: not the lender"); - require(tokenLoanParams[tokenId].expiration <= block.timestamp, "NFTPair: not expired"); + require( + // Addition is safe: both summands are smaller than 256 bits + uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp, + "NFTPair: not expired" + ); } // If there somehow is collateral but no accompanying loan, then anyone // can claim it by first requesting a loan with `skim` set to true, and @@ -280,7 +282,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // as good for the lender as `accepted`. require( params.valuation == accepted.valuation && - params.expiration <= accepted.expiration && + params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS, "NFTPair: bad params" ); @@ -333,11 +335,11 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // chain ID and master contract are a match, so we explicitly include the // clone address (and the asset/collateral addresses): - // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant LEND_SIGNATURE_HASH = 0x9bcf99059e3c2bf4522b1950b84c7777535a4a54075afadd793dc5f00a5e7aa9; + // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; - // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 expiration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0x2a060161383de688deddeac5b5c10a5ded88b7f78db776edf545b1e427997c09; + // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336; /// @notice Request and immediately borrow from a pre-committed lender @@ -372,7 +374,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { anyTokenId ? 0 : tokenId, anyTokenId, params.valuation, - params.expiration, + params.duration, params.annualInterestBPS, nonce, deadline @@ -408,7 +410,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address(this), tokenId, params.valuation, - params.expiration, + params.duration, params.annualInterestBPS, nonce, deadline @@ -504,7 +506,11 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - require(loanParams.expiration > block.timestamp, "NFTPair: loan expired"); + require( + // Addition is safe: both summands are smaller than 256 bits + uint256(loan.startTime) + loanParams.duration > block.timestamp, + "NFTPair: loan expired" + ); uint128 principal = loanParams.valuation; diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index 3abc5f10..2287fbc3 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -40,7 +40,7 @@ contract LendingClubMock { return requested.valuation == accepted.valuation && - requested.expiration <= accepted.expiration && + requested.duration <= accepted.duration && requested.annualInterestBPS >= accepted.annualInterestBPS; } @@ -59,8 +59,7 @@ contract LendingClubMock { // loans, and manipulating the token ID can only break the logic by // making the loan "safer" for the lender. conditions.valuation = uint128((tokenId + 1) * 10**18); - // Addition and cast are safe for the foreseeable future. - conditions.expiration = uint64(block.timestamp + 365 days); + conditions.duration = 365 days; conditions.annualInterestBPS = 10_000; } return conditions; diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index e77177d1..29a7f750 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -54,12 +54,12 @@ interface IPartialDeployParams { interface ILoanParams { valuation: BigNumber; - expiration: number; + duration: number; annualInterestBPS: number; } interface IPartialLoanParams { valuation?: BigNumber; - expiration?: number; + duration?: number; annualInterestBPS?: number; } @@ -112,7 +112,7 @@ describe("NFT Pair", async () => { const addToken = (pool, tokenId, params: IPartialLoanParams) => pool.connect(alice).updateLoanParams(tokenId, { valuation: 0, - expiration: nextYear, + duration: YEAR, openFeeBPS: 1000, annualInterestBPS: 2000, ...params, @@ -210,14 +210,14 @@ describe("NFT Pair", async () => { it("Should let anyone with an NFT request a loan against it", async () => { const params = { valuation: getBigNumber(10), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)) .to.emit(apes, "Transfer") .withArgs(alice.address, pair.address, apeIds.aliceOne) .to.emit(pair, "LogRequestLoan") - .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS); + .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.duration, params.annualInterestBPS); }); it("Should let anyone with an NFT request a loan (skim)", async () => { @@ -226,7 +226,7 @@ describe("NFT Pair", async () => { // the logic still works: const params = { valuation: getBigNumber(10), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; await apes.connect(alice).transferFrom(alice.address, pair.address, apeIds.aliceOne); @@ -236,7 +236,7 @@ describe("NFT Pair", async () => { it("Should fail to skim if token not present", async () => { const params = { valuation: getBigNumber(10), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, true)).to.be.revertedWith("NFTPair: skim failed"); @@ -245,7 +245,7 @@ describe("NFT Pair", async () => { it("Should refuse second request. Important if skimming!", async () => { const params = { valuation: getBigNumber(10), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; await expect(pair.connect(alice).requestLoan(apeIds.aliceOne, params, alice.address, false)).to.emit(pair, "LogRequestLoan"); @@ -255,7 +255,7 @@ describe("NFT Pair", async () => { it("Should refuse loan requests without collateral", async () => { const params = { valuation: getBigNumber(10), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; await expect(pair.connect(alice).requestLoan(apeIds.bobOne, params, alice.address, false)).to.be.revertedWith("From not owner"); @@ -277,7 +277,7 @@ describe("NFT Pair", async () => { params1 = { valuation: getBigNumber(1000), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; @@ -359,7 +359,7 @@ describe("NFT Pair", async () => { }); it("Should lend if expiration is earlier than expected", async () => { - const later = { ...params1, expiration: params1.expiration + 1 }; + const later = { ...params1, duration: params1.duration + 1 }; await expect(pair.connect(carol).lend(apeIds.aliceOne, later, false)).to.emit(pair, "LogLend"); }); @@ -380,7 +380,7 @@ describe("NFT Pair", async () => { }); it("Should NOT lend if expiration is later than expected", async () => { - const earlier = { ...params1, expiration: params1.expiration - 1 }; + const earlier = { ...params1, duration: params1.duration - 1 }; await expect(pair.connect(carol).lend(apeIds.aliceOne, earlier, false)).to.be.revertedWith("NFTPair: bad params"); }); @@ -417,7 +417,7 @@ describe("NFT Pair", async () => { params1 = { valuation: getBigNumber(1000), - expiration: tomorrow, + duration: DAY, annualInterestBPS: 2000, }; @@ -434,34 +434,34 @@ describe("NFT Pair", async () => { recordUpdate("valuation", (v) => v.sub(20_000_000)); recordUpdate("annualInterestBPS", (i) => i - 400); recordUpdate("annualInterestBPS", (i) => i + 300); - recordUpdate("expiration", (e) => e + 10_000); - recordUpdate("expiration", (e) => e - 98_765); + recordUpdate("duration", (d) => d + 10_000); + recordUpdate("duration", (d) => d - 9_876); for (const params of data) { await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params)) .to.emit(pair, "LogUpdateLoanParams") - .withArgs(apeIds.aliceOne, params.valuation, params.expiration, params.annualInterestBPS); + .withArgs(apeIds.aliceOne, params.valuation, params.duration, params.annualInterestBPS); } }); it("Should refuse updates to someone else's requests", async () => { - const params2 = { ...params1, expiration: params1.expiration + 2 }; + const params2 = { ...params1, duration: params1.duration + 2 }; await expect(pair.connect(bob).updateLoanParams(apeIds.aliceOne, params2)).to.be.revertedWith("NFTPair: not the borrower"); }); it("..even if you set the loan up for them", async () => { - const params2 = { ...params1, expiration: params1.expiration + 2 }; + const params2 = { ...params1, duration: params1.duration + 2 }; await pair.connect(bob).requestLoan(apeIds.bobOne, params1, alice.address, false); await expect(pair.connect(bob).updateLoanParams(apeIds.bobOne, params2)).to.be.revertedWith("NFTPair: not the borrower"); }); it("Should refuse updates to nonexisting loans", async () => { - const params2 = { ...params1, expiration: params1.expiration + 2 }; + const params2 = { ...params1, duration: params1.duration + 2 }; await expect(pair.connect(alice).updateLoanParams(apeIds.aliceTwo, params2)).to.be.revertedWith("NFTPair: no collateral"); }); it("Should refuse non-lender updates to outstanding loans", async () => { - const params2 = { ...params1, expiration: params1.expiration - 2 }; + const params2 = { ...params1, duration: params1.duration - 2 }; await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params2)).to.emit(pair, "LogUpdateLoanParams"); await pair.connect(carol).lend(apeIds.aliceOne, params2, false); @@ -481,7 +481,7 @@ describe("NFT Pair", async () => { }; recordUpdate("valuation", (v) => v.sub(10)); recordUpdate("annualInterestBPS", (i) => i - 400); - recordUpdate("expiration", (e) => e + 10_000); + recordUpdate("duration", (d) => d + 10_000); await pair.connect(carol).lend(apeIds.aliceOne, params1, false); @@ -497,7 +497,7 @@ describe("NFT Pair", async () => { }; recordUpdate("valuation", (v) => v.add(1)); recordUpdate("annualInterestBPS", (i) => i + 1); - recordUpdate("expiration", (e) => e - 1); + recordUpdate("duration", (d) => d - 1); await pair.connect(carol).lend(apeIds.aliceOne, params1, false); @@ -512,8 +512,9 @@ describe("NFT Pair", async () => { const params: ILoanParams = { valuation: getBigNumber(123), annualInterestBPS: 10_000, - expiration: Math.floor(new Date().getTime() / 1000) + 86400, + duration: DAY, }; + let startTime: number; before(async () => { pair = await deployPair(); @@ -526,6 +527,7 @@ describe("NFT Pair", async () => { await pair.connect(alice).requestLoan(id, params, alice.address, false); } await pair.connect(bob).lend(apeIds.aliceOne, params, false); + startTime = (await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber(); }); it("Should allow borrowers to remove unused collateral", async () => { @@ -545,7 +547,7 @@ describe("NFT Pair", async () => { }); it("Should allow lenders to seize collateral upon expiry", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); // Send it to someone else for a change: await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)) .to.emit(pair, "LogRemoveCollateral") @@ -555,15 +557,15 @@ describe("NFT Pair", async () => { }); it("Should not allow lenders to seize collateral otherwise", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration - 1]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not expired"); }); it("Should not allow others to seize collateral ever", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration - 1]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration + 1_000_000]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1_000_000]); await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); }); @@ -577,11 +579,12 @@ describe("NFT Pair", async () => { describeSnapshot("Repay", () => { let pair: NFTPair; + let startTime: number; const params: ILoanParams = { valuation: getBigNumber(1), annualInterestBPS: 10_000, - expiration: Math.floor(new Date().getTime() / 1000) + YEAR, + duration: YEAR, }; const valuationShare = params.valuation.mul(9).div(20); const borrowerShare = valuationShare.mul(99).div(100); @@ -611,6 +614,7 @@ describe("NFT Pair", async () => { await pair.connect(alice).requestLoan(id, params, alice.address, false); } await pair.connect(bob).lend(apeIds.aliceOne, params, false); + startTime = (await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber(); }); it("Should allow borrowers to pay off loans before expiry", async () => { @@ -758,7 +762,7 @@ describe("NFT Pair", async () => { const large: ILoanParams = { valuation: getBigNumber(1_000_000_000), annualInterestBPS: 65_535, - expiration: Math.floor(new Date().getTime() / 1000) + 2 * fiveYears, + duration: 2 * fiveYears, }; await pair.connect(alice).updateLoanParams(apeIds.aliceTwo, large); @@ -821,12 +825,12 @@ describe("NFT Pair", async () => { }); it("Should refuse repayments on expired loans", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); await expect(pair.connect(alice).repay(apeIds.aliceOne, false)).to.be.revertedWith("NFTPair: loan expired"); }); it("Should refuse repayments on nonexistent loans", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [params.expiration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); await expect(pair.connect(carol).repay(apeIds.carolOne, false)).to.be.revertedWith("NFTPair: no loan"); }); @@ -867,12 +871,12 @@ describe("NFT Pair", async () => { } }); - const signBorrowRequest = async (wallet, { tokenId, valuation, expiration, annualInterestBPS, deadline }) => { + const signBorrowRequest = async (wallet, { tokenId, valuation, duration, annualInterestBPS, deadline }) => { const sigTypes = [ { name: "contract", type: "address" }, { name: "tokenId", type: "uint256" }, { name: "valuation", type: "uint128" }, - { name: "expiration", type: "uint64" }, + { name: "duration", type: "uint64" }, { name: "annualInterestBPS", type: "uint16" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, @@ -886,7 +890,7 @@ describe("NFT Pair", async () => { contract: pair.address, tokenId, valuation, - expiration, + duration, annualInterestBPS, nonce: 0, deadline, @@ -916,13 +920,13 @@ describe("NFT Pair", async () => { return splitSignature(sig); }; - const signLendRequest = async (wallet, { tokenId, anyTokenId, valuation, expiration, annualInterestBPS, deadline }) => { + const signLendRequest = async (wallet, { tokenId, anyTokenId, valuation, duration, annualInterestBPS, deadline }) => { const sigTypes = [ { name: "contract", type: "address" }, { name: "tokenId", type: "uint256" }, { name: "anyTokenId", type: "bool" }, { name: "valuation", type: "uint128" }, - { name: "expiration", type: "uint64" }, + { name: "duration", type: "uint64" }, { name: "annualInterestBPS", type: "uint16" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, @@ -932,7 +936,7 @@ describe("NFT Pair", async () => { tokenId, anyTokenId, valuation, - expiration, + duration, annualInterestBPS, nonce: 0, deadline, @@ -961,7 +965,7 @@ describe("NFT Pair", async () => { // be taken up by anyone who can provide the token (and the signature). const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -969,7 +973,7 @@ describe("NFT Pair", async () => { tokenId: apeIds.carolOne, anyTokenId: false, valuation, - expiration, + duration, annualInterestBPS, deadline, }); @@ -982,7 +986,7 @@ describe("NFT Pair", async () => { apeIds.carolOne, bob.address, carol.address, - { valuation, expiration, annualInterestBPS }, + { valuation, duration, annualInterestBPS }, false, false, deadline, @@ -1001,7 +1005,7 @@ describe("NFT Pair", async () => { // be taken up by anyone who can provide the token (and the signature). const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -1009,7 +1013,7 @@ describe("NFT Pair", async () => { tokenId: 0, anyTokenId: true, valuation, - expiration, + duration, annualInterestBPS, deadline, }); @@ -1022,7 +1026,7 @@ describe("NFT Pair", async () => { apeIds.carolOne, bob.address, carol.address, - { valuation, expiration, annualInterestBPS }, + { valuation, duration, annualInterestBPS }, false, true, deadline, @@ -1038,7 +1042,7 @@ describe("NFT Pair", async () => { it("Should require an exact match on all conditions", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -1046,12 +1050,12 @@ describe("NFT Pair", async () => { tokenId: apeIds.carolOne, anyTokenId: false, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; // Carol tries to take the loan, but fails because oneo of the // parameters is different. This pretty much only tests that we do the // signature check at all, and it feels a bit silly to check every @@ -1071,7 +1075,7 @@ describe("NFT Pair", async () => { it("Should require the lender to be the signer", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -1079,12 +1083,12 @@ describe("NFT Pair", async () => { tokenId: apeIds.carolOne, anyTokenId: false, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; // Carol tries to take the loan from Alice instead and fails: await expect( pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, false, deadline, v, r, s) @@ -1094,7 +1098,7 @@ describe("NFT Pair", async () => { it("Should enforce the deadline", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -1102,12 +1106,12 @@ describe("NFT Pair", async () => { tokenId: apeIds.carolOne, anyTokenId: false, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; // Request fails because the deadline has expired: @@ -1118,7 +1122,7 @@ describe("NFT Pair", async () => { it("Should not accept the same signature twice", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; @@ -1126,12 +1130,12 @@ describe("NFT Pair", async () => { tokenId: apeIds.carolOne, anyTokenId: false, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; // It works the first time: @@ -1159,14 +1163,14 @@ describe("NFT Pair", async () => { // take it up - if they have the signature. const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, - expiration, + duration, annualInterestBPS, deadline, }); @@ -1175,7 +1179,7 @@ describe("NFT Pair", async () => { await expect( pair .connect(alice) - .takeCollateralAndLend(apeIds.bobTwo, bob.address, { valuation, expiration, annualInterestBPS }, false, deadline, v, r, s) + .takeCollateralAndLend(apeIds.bobTwo, bob.address, { valuation, duration, annualInterestBPS }, false, deadline, v, r, s) ) .to.emit(pair, "LogRequestLoan") .to.emit(pair, "LogLend"); @@ -1184,19 +1188,19 @@ describe("NFT Pair", async () => { it("Should require an exact match on all conditions", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; for (const [key, value] of Object.entries(loanParams)) { const altered = BigNumber.from(value).add(1); const badLoanParams = { ...loanParams, [key]: altered }; @@ -1209,19 +1213,19 @@ describe("NFT Pair", async () => { it("Should require the borrower to be the signer", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; // Alice tries to lend to Carol instead and fails: await expect( pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, carol.address, loanParams, false, deadline, v, r, s) @@ -1231,19 +1235,19 @@ describe("NFT Pair", async () => { it("Should enforce the deadline", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; await advanceNextTime(3601); await expect( @@ -1254,19 +1258,19 @@ describe("NFT Pair", async () => { it("Should not accept the same signature twice", async () => { const { timestamp } = await ethers.provider.getBlock("latest"); const valuation = getBigNumber(100); - const expiration = timestamp + 365 * 24 * 3600; + const duration = 365 * 24 * 3600; const annualInterestBPS = 15000; const deadline = timestamp + 3600; const { r, s, v } = await signBorrowRequest(bob, { tokenId: apeIds.bobTwo, valuation, - expiration, + duration, annualInterestBPS, deadline, }); - const loanParams = { valuation, expiration, annualInterestBPS }; + const loanParams = { valuation, duration, annualInterestBPS }; await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s)).to.emit( pair, @@ -1297,7 +1301,7 @@ describe("NFT Pair", async () => { const params: ILoanParams = { valuation: getBigNumber(3), annualInterestBPS: 5_000, - expiration: Math.floor(new Date().getTime() / 1000) + YEAR, + duration: YEAR, }; const valuationShare = params.valuation.mul(9).div(20); const borrowerShare = valuationShare.mul(99).div(100); @@ -1468,7 +1472,7 @@ describe("NFT Pair", async () => { values.push(0); datas.push( encodeParameters( - ["uint256", "tuple(uint128 valuation, uint64 expiration, uint16 annualInterestBPS)", "address", "bool"], + ["uint256", "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", "address", "bool"], [tokenId, params, recipient, skim] ) ); @@ -1483,7 +1487,7 @@ describe("NFT Pair", async () => { requestLoans((i) => [ { valuation: getBigNumber((i + 1) * 12), - expiration: nextYear, + duration: YEAR, annualInterestBPS: i * 500, }, [alice.address, bob.address, carol.address][i % 3], @@ -1497,11 +1501,11 @@ describe("NFT Pair", async () => { .to.emit(apes, "Transfer") .withArgs(bob.address, pair.address, tokenIds[9]) .to.emit(pair, "LogRequestLoan") - .withArgs(alice.address, tokenIds[6], getBigNumber(7 * 12), nextYear, 6 * 500) + .withArgs(alice.address, tokenIds[6], getBigNumber(7 * 12), YEAR, 6 * 500) .to.emit(pair, "LogRequestLoan") - .withArgs(bob.address, tokenIds[7], getBigNumber(8 * 12), nextYear, 7 * 500) + .withArgs(bob.address, tokenIds[7], getBigNumber(8 * 12), YEAR, 7 * 500) .to.emit(pair, "LogRequestLoan") - .withArgs(carol.address, tokenIds[8], getBigNumber(9 * 12), nextYear, 8 * 500); + .withArgs(carol.address, tokenIds[8], getBigNumber(9 * 12), YEAR, 8 * 500); }); it("Should have the expected domain separator", async () => { @@ -1547,7 +1551,7 @@ describe("NFT Pair", async () => { // await requestLoans((i) => [ // { // valuation: getBigNumber((i + 1) * 12), - // expiration: nextYear, + // duration: YEAR, // annualInterestBPS: i * 500, // }, // [alice.address, bob.address, carol.address][i % 3], @@ -1604,13 +1608,13 @@ describe("NFT Pair", async () => { expect(getBigNumber(apeIds.aliceOne, 0).mod(2)).to.equal(0); const valuation = getBigNumber(1).add(apeIds.aliceOne); - const expiration = nextWeek; + const duration = 7 * DAY; const annualInterestBPS = 20_000; await expect( borrow(alice, emptyLendingClub, apeIds.aliceOne, { valuation, - expiration, + duration, annualInterestBPS, }) ).to.be.revertedWith("NFTPair: LendingClub does not like you"); @@ -1618,7 +1622,7 @@ describe("NFT Pair", async () => { await expect( borrow(alice, lendingClub, apeIds.aliceOne, { valuation, - expiration, + duration, annualInterestBPS, }) ) @@ -1629,7 +1633,7 @@ describe("NFT Pair", async () => { await expect( borrow(alice, lendingClub, apeIds.aliceTwo, { valuation, - expiration, + duration, annualInterestBPS, }) ).to.be.revertedWith("NFTPair: LendingClub does not like you"); From bc882ec045ebef04bf50b77e164140d4c9f50dc6 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 14 Apr 2022 04:49:00 +0200 Subject: [PATCH 061/107] (Format LendingClubMock in line with the rest of the repo) --- contracts/mocks/LendingClubMock.sol | 34 +++++------------------------ 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index 2287fbc3..dfa5d872 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -16,21 +16,10 @@ contract LendingClubMock { } function init() public { - nftPair.bentoBox().setMasterContractApproval( - address(this), - address(nftPair.masterContract()), - true, - 0, - bytes32(0), - bytes32(0) - ); + nftPair.bentoBox().setMasterContractApproval(address(this), address(nftPair.masterContract()), true, 0, bytes32(0), bytes32(0)); } - function willLend(uint256 tokenId, TokenLoanParams memory requested) - external - view - returns (bool) - { + function willLend(uint256 tokenId, TokenLoanParams memory requested) external view returns (bool) { if (msg.sender != address(nftPair)) { return false; } @@ -44,11 +33,7 @@ contract LendingClubMock { requested.annualInterestBPS >= accepted.annualInterestBPS; } - function _lendingConditions(uint256 tokenId) - private - pure - returns (TokenLoanParams memory) - { + function _lendingConditions(uint256 tokenId) private pure returns (TokenLoanParams memory) { TokenLoanParams memory conditions; // No specific conditions given, but we'll take all even-numbered // ones at 100% APY: @@ -65,11 +50,7 @@ contract LendingClubMock { return conditions; } - function lendingConditions(address _nftPair, uint256 tokenId) - external - view - returns (TokenLoanParams memory) - { + function lendingConditions(address _nftPair, uint256 tokenId) external view returns (TokenLoanParams memory) { if (_nftPair != address(nftPair)) { TokenLoanParams memory empty; return empty; @@ -83,11 +64,6 @@ contract LendingClubMock { } function withdrawFunds(uint256 bentoShares) external { - nftPair.bentoBox().transfer( - nftPair.asset(), - address(this), - investor, - bentoShares - ); + nftPair.bentoBox().transfer(nftPair.asset(), address(this), investor, bentoShares); } } From a8bb75a28e2d8796cb846cdb67c9fc10ed5d693b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 14 Apr 2022 04:42:01 +0200 Subject: [PATCH 062/107] Cooks: Add ACTION_LEND and some tests --- contracts/NFTPair.sol | 4 + test/NFTPair.test.ts | 180 ++++++++++++++++++++++++++++-------------- 2 files changed, 126 insertions(+), 58 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 35d08f46..bc2b74ce 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -546,6 +546,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; uint8 internal constant ACTION_REQUEST_LOAN = 12; + uint8 internal constant ACTION_LEND = 13; // Function on BentoBox uint8 internal constant ACTION_BENTO_DEPOSIT = 20; @@ -651,6 +652,9 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { (uint256, TokenLoanParams, address, bool) ); requestLoan(tokenId, params, to, skim); + } else if (action == ACTION_LEND) { + (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool)); + lend(tokenId, params, skim); } else if (action == ACTION_BENTO_SETAPPROVAL) { (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( datas[i], diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 29a7f750..c1ae3f0e 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -25,6 +25,7 @@ const ACTION_REPAY = 2; const ACTION_REMOVE_COLLATERAL = 4; const ACTION_REQUEST_LOAN = 12; +const ACTION_LEND = 13; // Function on BentoBox const ACTION_BENTO_DEPOSIT = 20; @@ -1394,6 +1395,38 @@ describe("NFT Pair", async () => { let LEND_SIGNATURE_HASH: string; const tokenIds: BigNumber[] = []; + before(async () => { + chainId = (await ethers.provider.getNetwork()).chainId; + pair = await deployPair(); + // Undo some of the setup, so we start from scratch: + const mc = masterContract.address; + const hz = HashZero; + for (const signer of [alice, bob, carol]) { + const addr = signer.address; + const bb = bentoBox.connect(signer); + // In theory this can be done via a cook. In practice we're having some + // trouble with the EIP-712 (structured data) signature due to BentoBox + // using a slightly different encoding scheme than ethers.io expects. + // So, contract approval is assumed in our tests. + // await bb.setMasterContractApproval(addr, mc, false, 0, hz, hz); + + await guineas.transfer(addr, getBigNumber(10_000)); + // + await guineas.connect(signer).approve(bentoBox.address, MaxUint256); + + // We added profit in the setup; 3000 guineas became 3000 shares + await bb.withdraw(guineas.address, addr, addr, 0, getBigNumber(3000)); + } + expect(await bentoBox.balanceOf(guineas.address, alice.address)).to.equal(0); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + for (let i = 0; i < 10; i++) { + tokenIds.push(await mintApe(bob.address)); + } + }); const approveHash = hashUtf8String("Give FULL access to funds in (and approved to) BentoBox?"); const revokeHash = hashUtf8String("Revoke access to BentoBox?"); @@ -1432,35 +1465,7 @@ describe("NFT Pair", async () => { return splitSignature(sig); }; - before(async () => { - chainId = (await ethers.provider.getNetwork()).chainId; - pair = await deployPair(); - // Undo some of the setup, so we start from scratch: - const mc = masterContract.address; - const hz = HashZero; - for (const signer of [alice, bob, carol]) { - const addr = signer.address; - const bb = bentoBox.connect(signer); - await bb.setMasterContractApproval(addr, mc, false, 0, hz, hz); - - await guineas.transfer(addr, getBigNumber(10_000)); - await guineas.connect(signer).approve(bentoBox.address, MaxUint256); - - // We added profit in the setup; 3000 guineas became 3000 shares - await bb.withdraw(guineas.address, addr, addr, 0, getBigNumber(3000)); - } - expect(await bentoBox.balanceOf(guineas.address, alice.address)).to.equal(0); - - // (No permit equivalent that we could do via a cook..) - for (const signer of [deployer, alice, bob, carol]) { - await apes.connect(signer).setApprovalForAll(pair.address, true); - } - - for (let i = 0; i < 10; i++) { - tokenIds.push(await mintApe(bob.address)); - } - }); - + // Bob is hardcoded as the borrower const requestLoans = (getArgs: (i: number) => [ILoanParams, string, boolean]) => { const actions: number[] = []; const values: any[] = []; @@ -1480,6 +1485,45 @@ describe("NFT Pair", async () => { return pair.connect(bob).cook(actions, values, datas); }; + // Alice is hardcoded as the lender + const issueLoans = (getArgs: (i: number) => [ILoanParams, boolean]) => { + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // 1. Set contract approval. SKIPPING involuntarily: + // const { v, r, s } = await signBentoApprovalRequest(alice, true); + // actions.push(ACTION_BENTO_SETAPPROVAL); + // values.push(0); + // datas.push(encodeParameters( + // ["address", "address", "bool", "uint8", "bytes32", "bytes32"], + // [alice.address, masterContract.address, true, v, r, s] + // )); + + // 2. Deposit funds into BentoBox + const n = tokenIds.length; + expect(n).to.be.gte(2); + const amountNeeded = getBigNumber(n * (n + 1) * 6); + actions.push(ACTION_BENTO_DEPOSIT); + values.push(0); // TODO: Test with ETH as the token + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, alice.address, amountNeeded, 0])); + + // 3. Lend + for (let i = 0; i < n; i++) { + actions.push(ACTION_LEND); + values.push(0); + const [params, skim] = getArgs(i); + datas.push( + encodeParameters( + ["uint256", "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", "bool"], + [tokenIds[i], params, skim] + ) + ); + } + + return pair.connect(alice).cook(actions, values, datas); + }; + // Suppose this is one use case.. it("Should allow requesting multiple loans", async () => { // All apes come from Bob: @@ -1547,35 +1591,55 @@ describe("NFT Pair", async () => { // Failing the approval request; types in sig not an exact match, so have to // sign the message without help from ethers' abstractions. See also the // DOMAIN_SEPARATOR test; suspect a similar issue. - // it("Should handle depositing and lending with minimal setup", async () => { - // await requestLoans((i) => [ - // { - // valuation: getBigNumber((i + 1) * 12), - // duration: YEAR, - // annualInterestBPS: i * 500, - // }, - // [alice.address, bob.address, carol.address][i % 3], - // false, - // ]); - - // const actions: number[] = []; - // const values: any[] = []; - // const datas: any[] = []; - - // // 1. Set contract approval (can check if needed) - // const { v, r, s } = await signBentoApprovalRequest(alice, true); - // actions.push(ACTION_BENTO_SETAPPROVAL); - // values.push(0); - // datas.push(encodeParameters( - // ["address", "address", "bool", "uint8", "bytes32", "bytes32"], - // [alice.address, masterContract.address, true, v, r, s] - // )); - - // // 2. Bento permit - // // 3. Bento deposit - // // 4. Lend - // await pair.connect(alice).cook(actions, values, datas); - // }); + it("Should handle depositing and lending with minimal setup", async () => { + // Bob requests loans for himself: + await requestLoans((i) => [ + { + valuation: getBigNumber((i + 1) * 12), + duration: YEAR, + annualInterestBPS: i * 500, + }, + bob.address, + false, + ]); + await expect( + issueLoans((i) => [ + { + valuation: getBigNumber((i + 1) * 12), + duration: YEAR, + annualInterestBPS: i * 500, + }, + false, + ]) + ) + .to.emit(pair, "LogLend") + .withArgs(alice.address, tokenIds[0]); + }); + + it("Should revert the entire cook if one transaction fails", async () => { + await requestLoans((i) => [ + { + valuation: getBigNumber((i + 1) * 12), + duration: YEAR, + annualInterestBPS: i * 500, + }, + bob.address, + false, + ]); + + await expect( + issueLoans((i) => [ + { + valuation: getBigNumber((i + 1) * 12), + duration: YEAR, + // Last request is bad in that the lender now wants more interest + // than the borrower is willing to pay: + annualInterestBPS: i * 500 + (i == tokenIds.length - 1 ? 1 : 0), + }, + false, + ]) + ).to.be.revertedWith("NFTPair: bad params"); + }); }); describeSnapshot("Lending Club", () => { From 7783787ee25cccaad2868b95f47320969718be50 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 06:43:24 +0200 Subject: [PATCH 063/107] (Add deployment script for NFTPair master contract) --- deploy/NFTPair.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 deploy/NFTPair.ts diff --git a/deploy/NFTPair.ts b/deploy/NFTPair.ts new file mode 100644 index 00000000..3d82e0b0 --- /dev/null +++ b/deploy/NFTPair.ts @@ -0,0 +1,41 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { ethers, network } from "hardhat"; +import { BentoBoxMock } from "../typechain"; +import { DeploymentSubmission } from "hardhat-deploy/dist/types"; +import { expect } from "chai"; + +const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + + const { deployer } = await getNamedAccounts(); + + // Original BentoBox on mainnet: + const bentoBoxAddress = "0xf5bce5077908a1b7370b9ae04adc565ebd643966"; + + await deploy("NFTPair", { + from: deployer, + args: [bentoBoxAddress], + log: true, + deterministicDeployment: false, + }); +}; + +export default deployFunction; + +if (network.name !== "hardhat" || process.env.HARDHAT_LOCAL_NODE) { + deployFunction.skip = ({ getChainId }) => + new Promise((resolve, reject) => { + try { + getChainId().then((chainId) => { + resolve(chainId !== "1"); + }); + } catch (error) { + reject(error); + } + }); +} + +deployFunction.tags = ["NFTPair"]; +deployFunction.dependencies = []; From c6801ea8fecd31a0d0713325f3855b1a185e5512 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 06:43:40 +0200 Subject: [PATCH 064/107] (Format/clean up PrivatePool deploy script) --- deploy/PrivatePool.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deploy/PrivatePool.ts b/deploy/PrivatePool.ts index 4c6f7225..87541c15 100644 --- a/deploy/PrivatePool.ts +++ b/deploy/PrivatePool.ts @@ -5,9 +5,7 @@ import { BentoBoxMock } from "../typechain"; import { DeploymentSubmission } from "hardhat-deploy/dist/types"; import { expect } from "chai"; -const deployFunction: DeployFunction = async function ( - hre: HardhatRuntimeEnvironment -) { +const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deploy } = deployments; @@ -27,7 +25,6 @@ const deployFunction: DeployFunction = async function ( export default deployFunction; if (network.name !== "hardhat" || process.env.HARDHAT_LOCAL_NODE) { - console.log("SKI-AP!"); deployFunction.skip = ({ getChainId }) => new Promise((resolve, reject) => { try { From 6eac3852f401a7d2b8eb9e02a80e95aae65fcf41 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 06:45:27 +0200 Subject: [PATCH 065/107] (Apply standard formatting to some config files) --- commitlint.config.js | 4 ++-- hardhat.config.ts | 10 +++++----- tsconfig.json | 4 +--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index ed123736..69b4242c 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'] -}; \ No newline at end of file + extends: ["@commitlint/config-conventional"], +}; diff --git a/hardhat.config.ts b/hardhat.config.ts index a8db04a7..36b901f6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,7 +5,7 @@ import "@nomiclabs/hardhat-solhint"; import "@nomiclabs/hardhat-ethers"; import "@nomiclabs/hardhat-waffle"; import "@typechain/hardhat"; -import "@tenderly/hardhat-tenderly" +import "@tenderly/hardhat-tenderly"; import "hardhat-abi-exporter"; import "hardhat-gas-reporter"; import "solidity-coverage"; @@ -43,8 +43,8 @@ const config: HardhatUserConfig = { arbitrumOne: process.env.ETHERSCAN_TOKEN, avalanche: process.env.SNOWTRACE_TOKEN, opera: process.env.FTMSCAN_TOKEN, - bsc: process.env.BSCSCAN_TOKEN - } + bsc: process.env.BSCSCAN_TOKEN, + }, }, gasReporter: { coinmarketcap: process.env.COINMARKETCAP_API_KEY, @@ -210,8 +210,8 @@ const config: HardhatUserConfig = { bail: true, }, tenderly: { - project: process.env.TENDERLY_PROJECT || 'project', - username: process.env.TENDERLY_USERNAME || '', + project: process.env.TENDERLY_PROJECT || "project", + username: process.env.TENDERLY_USERNAME || "", }, solidity: { compilers: [ diff --git a/tsconfig.json b/tsconfig.json index 9a4e9b85..708f6d13 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,6 @@ "./typechain", "./examples" ], - "exclude": [ - "./archive" - ], + "exclude": ["./archive"], "files": ["hardhat.config.ts"] } From 8e370fccbdb8cd0acbf3ffb5eb2a0043d729b927 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 06:45:33 +0200 Subject: [PATCH 066/107] Deploy a mock NFTPair ecosystem on Ropsten --- deploy/MockNFTPairEcoSystem.ts | 106 ++ deployments/ropsten/.chainId | 1 + .../ropsten/ApesGuineasNFTPairMock.json | 4 + deployments/ropsten/ApesNFTMock.json | 582 ++++++ deployments/ropsten/ApesWethNFTPairMock.json | 4 + .../ropsten/BearsGuineasNFTPairMock.json | 4 + deployments/ropsten/BearsNFTMock.json | 582 ++++++ deployments/ropsten/BentoBoxMock.json | 1681 +++++++++++++++++ deployments/ropsten/GuineasMock.json | 428 +++++ deployments/ropsten/NFTPair.json | 1179 ++++++++++++ deployments/ropsten/NFTPairMock.json | 1179 ++++++++++++ deployments/ropsten/WETH9Mock.json | 396 ++++ .../3285d4ce4a1fc523877d40dd9fd97bbb.json | 158 ++ .../6c88ba2f34cfbd7cd92d738b883e2e91.json | 35 + .../aca01c1a6fd2699b1adc885469d39e7b.json | 38 + 15 files changed, 6377 insertions(+) create mode 100644 deploy/MockNFTPairEcoSystem.ts create mode 100644 deployments/ropsten/.chainId create mode 100644 deployments/ropsten/ApesGuineasNFTPairMock.json create mode 100644 deployments/ropsten/ApesNFTMock.json create mode 100644 deployments/ropsten/ApesWethNFTPairMock.json create mode 100644 deployments/ropsten/BearsGuineasNFTPairMock.json create mode 100644 deployments/ropsten/BearsNFTMock.json create mode 100644 deployments/ropsten/BentoBoxMock.json create mode 100644 deployments/ropsten/GuineasMock.json create mode 100644 deployments/ropsten/NFTPair.json create mode 100644 deployments/ropsten/NFTPairMock.json create mode 100644 deployments/ropsten/WETH9Mock.json create mode 100644 deployments/ropsten/solcInputs/3285d4ce4a1fc523877d40dd9fd97bbb.json create mode 100644 deployments/ropsten/solcInputs/6c88ba2f34cfbd7cd92d738b883e2e91.json create mode 100644 deployments/ropsten/solcInputs/aca01c1a6fd2699b1adc885469d39e7b.json diff --git a/deploy/MockNFTPairEcoSystem.ts b/deploy/MockNFTPairEcoSystem.ts new file mode 100644 index 00000000..c545b87a --- /dev/null +++ b/deploy/MockNFTPairEcoSystem.ts @@ -0,0 +1,106 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { ethers, network } from "hardhat"; +import { BentoBoxMock } from "../typechain"; +import { ChainId } from "../utilities"; +import { DeploymentSubmission } from "hardhat-deploy/dist/types"; +import { expect } from "chai"; + +const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + + const { deployer } = await getNamedAccounts(); + + const wethMock = await deploy("WETH9Mock", { + from: deployer, + args: [], + log: true, + deterministicDeployment: false, + }); + const bentoBoxMock = await deploy("BentoBoxMock", { + from: deployer, + args: [wethMock.address], + log: true, + deterministicDeployment: false, + }); + const bentoBox = await ethers.getContractAt("BentoBoxMock", bentoBoxMock.address); + + // Master contract + const nftPairMock = await deploy("NFTPairMock", { + contract: "NFTPair", + from: deployer, + args: [bentoBoxMock.address], + log: true, + deterministicDeployment: false, + }); + + // Mock tokens + const apesMock = await deploy("ApesNFTMock", { + contract: "ERC721Mock", + from: deployer, + args: [], + log: true, + deterministicDeployment: false, + }); + const bearsMock = await deploy("BearsNFTMock", { + contract: "ERC721Mock", + from: deployer, + args: [], + log: true, + deterministicDeployment: false, + }); + const MaxUint128 = ethers.BigNumber.from(2).pow(128).sub(1); + const guineasMock = await deploy("GuineasMock", { + contract: "ERC20Mock", + from: deployer, + args: [MaxUint128], + log: true, + deterministicDeployment: false, + }); + + // Pairs - deployed by BentoBox: + const bentoDeploy = async (name, masterAddress, initData) => { + try { + await deployments.get(name); + return; + } catch {} + const deployTx = await bentoBox.deploy(masterAddress, initData, true).then((tx) => tx.wait()); + for (const e of deployTx.events || []) { + if (e.eventSignature == "LogDeploy(address,bytes,address)") { + await deployments.save(name, { + abi: [], + address: e.args?.cloneAddress, + }); + } + return; + } + throw new Error("Failed to either find or execute deployment"); + }; + const deployPair = (name, collateral, asset) => + bentoDeploy(name, nftPairMock.address, ethers.utils.defaultAbiCoder.encode(["address", "address"], [collateral.address, asset.address])); + + await deployPair("ApesGuineasNFTPairMock", apesMock, guineasMock); + await deployPair("ApesWethNFTPairMock", apesMock, wethMock); + await deployPair("BearsGuineasNFTPairMock", bearsMock, guineasMock); +}; + +export default deployFunction; + +const testChainIds = [ChainId.Ropsten, ChainId.Rinkeby, ChainId.Goerli, ChainId.Kovan, ChainId.BSCTestnet, ChainId.Localhost, ChainId.Hardhat]; + +if (network.name !== "hardhat" || process.env.HARDHAT_LOCAL_NODE) { + deployFunction.skip = ({ getChainId }) => + new Promise((resolve, reject) => { + try { + getChainId().then((chainId) => { + resolve(!testChainIds.includes(parseInt(chainId, 10))); + }); + } catch (error) { + reject(error); + } + }); +} + +deployFunction.tags = ["NFTPair"]; +deployFunction.dependencies = []; diff --git a/deployments/ropsten/.chainId b/deployments/ropsten/.chainId new file mode 100644 index 00000000..e440e5c8 --- /dev/null +++ b/deployments/ropsten/.chainId @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/deployments/ropsten/ApesGuineasNFTPairMock.json b/deployments/ropsten/ApesGuineasNFTPairMock.json new file mode 100644 index 00000000..3b72d2ee --- /dev/null +++ b/deployments/ropsten/ApesGuineasNFTPairMock.json @@ -0,0 +1,4 @@ +{ + "address": "0xD544Bcf7c7479d29438Da7bA168A34b4DfefA3CF", + "abi": [] +} \ No newline at end of file diff --git a/deployments/ropsten/ApesNFTMock.json b/deployments/ropsten/ApesNFTMock.json new file mode 100644 index 00000000..4313e710 --- /dev/null +++ b/deployments/ropsten/ApesNFTMock.json @@ -0,0 +1,582 @@ +{ + "address": "0xcCB893B3b5D7B003FEA0134215E4BCc6F8fb6aC7", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "approved", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceID", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokensOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } + ], + "transactionHash": "0x9f0bc9365e38ad2006bf4729d1eae00e50faf1e3c7c6c9e3973ee2f4735cad5c", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0xcCB893B3b5D7B003FEA0134215E4BCc6F8fb6aC7", + "transactionIndex": 0, + "gasUsed": "864780", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0xbf55b0cbb7b4062d378cf8206416749838b0736916fce105ad6c9d9dfa75c891", + "transactionHash": "0x9f0bc9365e38ad2006bf4729d1eae00e50faf1e3c7c6c9e3973ee2f4735cad5c", + "logs": [], + "blockNumber": 12212439, + "cumulativeGasUsed": "864780", + "status": 1, + "byzantium": true + }, + "args": [], + "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"ApprovalForAll\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"getApproved\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isApprovedForAll\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"mint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"ownerOf\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"setApprovalForAll\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceID\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"tokenByIndex\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"tokenOfOwnerByIndex\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"tokenURI\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokensOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/ERC721Mock.sol\":\"ERC721Mock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport \\\"./libraries/BoringAddress.sol\\\";\\nimport \\\"./libraries/BoringMath.sol\\\";\\nimport \\\"./interfaces/IERC721TokenReceiver.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nabstract contract BoringMultipleNFT {\\n /// This contract is an EIP-721 compliant contract with enumerable support\\n /// To optimize for gas, tokenId is sequential and start at 0. Also, tokens can't be removed/burned.\\n using BoringAddress for address;\\n using BoringMath for uint256;\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n uint256 public totalSupply = 0;\\n\\n struct TokenInfo {\\n address owner;\\n uint24 index; // index in the tokensOf array, one address can hold a maximum of 16,777,216 tokens\\n uint72 data; // data field can be usse to store traits\\n }\\n\\n // operator mappings as per usual\\n mapping(address => mapping(address => bool)) public isApprovedForAll;\\n mapping(address => uint256[]) public tokensOf; // Array of tokens owned by\\n mapping(uint256 => TokenInfo) internal _tokens; // The index in the tokensOf array for the token, needed to remove tokens from tokensOf\\n mapping(uint256 => address) internal _approved; // keep track of approved nft\\n\\n function supportsInterface(bytes4 interfaceID) external pure returns (bool) {\\n return\\n interfaceID == this.supportsInterface.selector || // EIP-165\\n interfaceID == 0x80ac58cd; // EIP-721\\n }\\n\\n function approve(address approved, uint256 tokenId) public payable {\\n address owner = _tokens[tokenId].owner;\\n require(msg.sender == owner || isApprovedForAll[owner][msg.sender], \\\"Not allowed\\\");\\n _approved[tokenId] = approved;\\n emit Approval(owner, approved, tokenId);\\n }\\n\\n function getApproved(uint256 tokenId) public view returns (address approved) {\\n require(tokenId < totalSupply, \\\"Invalid tokenId\\\");\\n return _approved[tokenId];\\n }\\n\\n function setApprovalForAll(address operator, bool approved) public {\\n isApprovedForAll[msg.sender][operator] = approved;\\n emit ApprovalForAll(msg.sender, operator, approved);\\n }\\n\\n function ownerOf(uint256 tokenId) public view returns (address) {\\n address owner = _tokens[tokenId].owner;\\n require(owner != address(0), \\\"No owner\\\");\\n return owner;\\n }\\n\\n function balanceOf(address owner) public view returns (uint256) {\\n require(owner != address(0), \\\"No 0 owner\\\");\\n return tokensOf[owner].length;\\n }\\n\\n function _transferBase(\\n uint256 tokenId,\\n address from,\\n address to,\\n uint72 data\\n ) internal {\\n address owner = _tokens[tokenId].owner;\\n require(from == owner, \\\"From not owner\\\");\\n\\n uint24 index;\\n // Remove the token from the current owner's tokensOf array\\n if (from != address(0)) {\\n index = _tokens[tokenId].index; // The index of the item to remove in the array\\n data = _tokens[tokenId].data;\\n uint256 last = tokensOf[from].length - 1;\\n uint256 lastTokenId = tokensOf[from][last];\\n tokensOf[from][index] = lastTokenId; // Copy the last item into the slot of the one to be removed\\n _tokens[lastTokenId].index = index; // Update the token index for the last item that was moved\\n tokensOf[from].pop(); // Delete the last item\\n }\\n\\n index = uint24(tokensOf[to].length);\\n tokensOf[to].push(tokenId);\\n _tokens[tokenId] = TokenInfo({owner: to, index: index, data: data});\\n\\n // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event.\\n _approved[tokenId] = address(0);\\n emit Transfer(from, to, tokenId);\\n }\\n\\n function _transfer(\\n address from,\\n address to,\\n uint256 tokenId\\n ) internal {\\n require(msg.sender == from || msg.sender == _approved[tokenId] || isApprovedForAll[from][msg.sender], \\\"Transfer not allowed\\\");\\n require(to != address(0), \\\"No zero address\\\");\\n // check for owner == from is in base\\n _transferBase(tokenId, from, to, 0);\\n }\\n\\n function transferFrom(\\n address from,\\n address to,\\n uint256 tokenId\\n ) public payable {\\n _transfer(from, to, tokenId);\\n }\\n\\n function safeTransferFrom(\\n address from,\\n address to,\\n uint256 tokenId\\n ) public payable {\\n safeTransferFrom(from, to, tokenId, \\\"\\\");\\n }\\n\\n function safeTransferFrom(\\n address from,\\n address to,\\n uint256 tokenId,\\n bytes memory data\\n ) public payable {\\n _transfer(from, to, tokenId);\\n if (to.isContract()) {\\n require(\\n IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) ==\\n bytes4(keccak256(\\\"onERC721Received(address,address,uint256,bytes)\\\")),\\n \\\"Wrong return value\\\"\\n );\\n }\\n }\\n\\n function tokenURI(uint256 tokenId) public view returns (string memory) {\\n require(tokenId < totalSupply, \\\"Not minted\\\");\\n return _tokenURI(tokenId);\\n }\\n\\n function _tokenURI(uint256 tokenId) internal view virtual returns (string memory);\\n\\n function tokenByIndex(uint256 index) public view returns (uint256) {\\n require(index < totalSupply, \\\"Out of bounds\\\");\\n return index; // This works due the optimization of sequential tokenIds and no burning\\n }\\n\\n function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) {\\n return tokensOf[owner][index];\\n }\\n\\n function _mint(address owner, uint72 data) internal {\\n _transferBase(totalSupply, address(0), owner, data);\\n totalSupply++;\\n }\\n}\\n\",\"keccak256\":\"0x2c90ee1540ed6bd1af2a98eba7aa99b8b528bd04ec558d1599fd1d71b4e667ca\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC721TokenReceiver.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC721TokenReceiver {\\n function onERC721Received(\\n address _operator,\\n address _from,\\n uint256 _tokenId,\\n bytes calldata _data\\n ) external returns (bytes4);\\n}\\n\",\"keccak256\":\"0x6dbac0edb2e4c5c806089a47a9819b21ca37f0eefe7a3b8cb4090b78f788c02b\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n\\n//SPDX-License-Identifier: MIT\\npragma solidity ^0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\nlibrary BoringAddress {\\n function isContract(address account) internal view returns (bool) {\\n uint256 size;\\n assembly {\\n size := extcodesize(account)\\n }\\n return size > 0;\\n }\\n}\\n\",\"keccak256\":\"0xcac0ac5a21ec520c48fb364204da3ae55460e46b0344d010583564d367deb030\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"contracts/mocks/ERC721Mock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\\\";\\n\\ncontract ERC721Mock is BoringMultipleNFT {\\n function mint(address owner) public returns (uint256 id) {\\n id = totalSupply;\\n _mint(owner, 0);\\n }\\n\\n function _tokenURI(uint256) internal view override returns (string memory) {\\n return \\\"\\\";\\n }\\n}\\n\",\"keccak256\":\"0xa99ad0ed4051c90f4ee7d6b0cef9a7f250cea5b7b519b9766696b297f2bc27fd\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60806040526000805534801561001457600080fd5b50610ea4806100246000396000f3fe6080604052600436106100f35760003560e01c80634f6ccce71161008a578063a22cb46511610059578063a22cb46514610373578063b88d4fde146103ae578063c87b56dd14610474578063e985e9c514610513576100f3565b80634f6ccce7146102b95780636352211e146102e35780636a6278421461030d57806370a0823114610340576100f3565b806323b872dd116100c657806323b872dd146101db5780632f745c591461021157806338a3dbae1461024a57806342842e0e14610283576100f3565b806301ffc9a7146100f8578063081812fc14610140578063095ea7b31461018657806318160ddd146101b4575b600080fd5b34801561010457600080fd5b5061012c6004803603602081101561011b57600080fd5b50356001600160e01b03191661054e565b604080519115158252519081900360200190f35b34801561014c57600080fd5b5061016a6004803603602081101561016357600080fd5b5035610587565b604080516001600160a01b039092168252519081900360200190f35b6101b26004803603604081101561019c57600080fd5b506001600160a01b0381351690602001356105ec565b005b3480156101c057600080fd5b506101c96106d0565b60408051918252519081900360200190f35b6101b2600480360360608110156101f157600080fd5b506001600160a01b038135811691602081013590911690604001356106d6565b34801561021d57600080fd5b506101c96004803603604081101561023457600080fd5b506001600160a01b0381351690602001356106e6565b34801561025657600080fd5b506101c96004803603604081101561026d57600080fd5b506001600160a01b03813516906020013561071d565b6101b26004803603606081101561029957600080fd5b506001600160a01b0381358116916020810135909116906040013561074b565b3480156102c557600080fd5b506101c9600480360360208110156102dc57600080fd5b5035610766565b3480156102ef57600080fd5b5061016a6004803603602081101561030657600080fd5b50356107b1565b34801561031957600080fd5b506101c96004803603602081101561033057600080fd5b50356001600160a01b0316610806565b34801561034c57600080fd5b506101c96004803603602081101561036357600080fd5b50356001600160a01b0316610817565b34801561037f57600080fd5b506101b26004803603604081101561039657600080fd5b506001600160a01b038135169060200135151561087d565b6101b2600480360360808110156103c457600080fd5b6001600160a01b038235811692602081013590911691604082013591908101906080810160608201356401000000008111156103ff57600080fd5b82018360208201111561041157600080fd5b8035906020019184600183028401116401000000008311171561043357600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506108eb945050505050565b34801561048057600080fd5b5061049e6004803603602081101561049757600080fd5b5035610a5a565b6040805160208082528351818301528351919283929083019185019080838360005b838110156104d85781810151838201526020016104c0565b50505050905090810190601f1680156105055780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561051f57600080fd5b5061012c6004803603604081101561053657600080fd5b506001600160a01b0381358116916020013516610aa8565b60006001600160e01b031982166301ffc9a760e01b148061057f57506380ac58cd60e01b6001600160e01b03198316145b90505b919050565b6000805482106105d0576040805162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a59081d1bdad95b9259608a1b604482015290519081900360640190fd5b506000908152600460205260409020546001600160a01b031690565b6000818152600360205260409020546001600160a01b03163381148061063557506001600160a01b038116600090815260016020908152604080832033845290915290205460ff165b610674576040805162461bcd60e51b815260206004820152600b60248201526a139bdd08185b1b1bddd95960aa1b604482015290519081900360640190fd5b60008281526004602052604080822080546001600160a01b0319166001600160a01b0387811691821790925591518593918516917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a4505050565b60005481565b6106e1838383610ac8565b505050565b6001600160a01b038216600090815260026020526040812080548390811061070a57fe5b9060005260206000200154905092915050565b6002602052816000526040600020818154811061073657fe5b90600052602060002001600091509150505481565b6106e1838383604051806020016040528060008152506108eb565b6000805482106107ad576040805162461bcd60e51b815260206004820152600d60248201526c4f7574206f6620626f756e647360981b604482015290519081900360640190fd5b5090565b6000818152600360205260408120546001600160a01b03168061057f576040805162461bcd60e51b815260206004820152600860248201526727379037bbb732b960c11b604482015290519081900360640190fd5b600080549050610582826000610bc5565b60006001600160a01b038216610861576040805162461bcd60e51b815260206004820152600a602482015269273790181037bbb732b960b11b604482015290519081900360640190fd5b506001600160a01b031660009081526002602052604090205490565b3360008181526001602090815260408083206001600160a01b03871680855290835292819020805460ff1916861515908117909155815190815290519293927f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31929181900390910190a35050565b6108f6848484610ac8565b610908836001600160a01b0316610be1565b15610a5457604051630a85bd0160e11b80825233600483018181526001600160a01b0388811660248601526044850187905260806064860190815286516084870152865194959189169463150b7a0294938b938a938a93909160a40190602085019080838360005b83811015610988578181015183820152602001610970565b50505050905090810190601f1680156109b55780820380516001836020036101000a031916815260200191505b5095505050505050602060405180830381600087803b1580156109d757600080fd5b505af11580156109eb573d6000803e3d6000fd5b505050506040513d6020811015610a0157600080fd5b50516001600160e01b03191614610a54576040805162461bcd60e51b815260206004820152601260248201527157726f6e672072657475726e2076616c756560701b604482015290519081900360640190fd5b50505050565b60606000548210610a9f576040805162461bcd60e51b815260206004820152600a602482015269139bdd081b5a5b9d195960b21b604482015290519081900360640190fd5b61057f82610be7565b600160209081526000928352604080842090915290825290205460ff1681565b336001600160a01b0384161480610af557506000818152600460205260409020546001600160a01b031633145b80610b2357506001600160a01b038316600090815260016020908152604080832033845290915290205460ff165b610b6b576040805162461bcd60e51b8152602060048201526014602482015273151c985b9cd9995c881b9bdd08185b1b1bddd95960621b604482015290519081900360640190fd5b6001600160a01b038216610bb8576040805162461bcd60e51b815260206004820152600f60248201526e4e6f207a65726f206164647265737360881b604482015290519081900360640190fd5b6106e18184846000610bfa565b610bd460005460008484610bfa565b5050600080546001019055565b3b151590565b5060408051602081019091526000815290565b6000848152600360205260409020546001600160a01b039081169084168114610c5b576040805162461bcd60e51b815260206004820152600e60248201526d233937b6903737ba1037bbb732b960911b604482015290519081900360640190fd5b60006001600160a01b03851615610d7857506000858152600360209081526040808320546001600160a01b0388168452600290925282208054600160b81b830468ffffffffffffffffff169550600160a01b90920462ffffff169260001983019290919083908110610cc957fe5b906000526020600020015490508060026000896001600160a01b03166001600160a01b031681526020019081526020016000208462ffffff1681548110610d0c57fe5b600091825260208083209190910192909255828152600382526040808220805462ffffff60a01b1916600160a01b62ffffff8916021790556001600160a01b038a168252600290925220805480610d5f57fe5b6001900381819060005260206000200160009055905550505b506001600160a01b038084166000818152600260209081526040808320805460018101825590845282842081018b9055815160608101835285815262ffffff80831682860190815268ffffffffffffffffff808c168487019081528f8952600388528689209451855493519151909216600160b81b026001600160b81b0391909416600160a01b0262ffffff60a01b19928c166001600160a01b0319948516179290921691909117169190911790915560049093528184208054909316909255519093899392908916917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9190a450505050505056fea26469706673582212208b37a2e45a2fde41f60dbba03559ab5c06eea496b21595a80cd23339dac442f164736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106100f35760003560e01c80634f6ccce71161008a578063a22cb46511610059578063a22cb46514610373578063b88d4fde146103ae578063c87b56dd14610474578063e985e9c514610513576100f3565b80634f6ccce7146102b95780636352211e146102e35780636a6278421461030d57806370a0823114610340576100f3565b806323b872dd116100c657806323b872dd146101db5780632f745c591461021157806338a3dbae1461024a57806342842e0e14610283576100f3565b806301ffc9a7146100f8578063081812fc14610140578063095ea7b31461018657806318160ddd146101b4575b600080fd5b34801561010457600080fd5b5061012c6004803603602081101561011b57600080fd5b50356001600160e01b03191661054e565b604080519115158252519081900360200190f35b34801561014c57600080fd5b5061016a6004803603602081101561016357600080fd5b5035610587565b604080516001600160a01b039092168252519081900360200190f35b6101b26004803603604081101561019c57600080fd5b506001600160a01b0381351690602001356105ec565b005b3480156101c057600080fd5b506101c96106d0565b60408051918252519081900360200190f35b6101b2600480360360608110156101f157600080fd5b506001600160a01b038135811691602081013590911690604001356106d6565b34801561021d57600080fd5b506101c96004803603604081101561023457600080fd5b506001600160a01b0381351690602001356106e6565b34801561025657600080fd5b506101c96004803603604081101561026d57600080fd5b506001600160a01b03813516906020013561071d565b6101b26004803603606081101561029957600080fd5b506001600160a01b0381358116916020810135909116906040013561074b565b3480156102c557600080fd5b506101c9600480360360208110156102dc57600080fd5b5035610766565b3480156102ef57600080fd5b5061016a6004803603602081101561030657600080fd5b50356107b1565b34801561031957600080fd5b506101c96004803603602081101561033057600080fd5b50356001600160a01b0316610806565b34801561034c57600080fd5b506101c96004803603602081101561036357600080fd5b50356001600160a01b0316610817565b34801561037f57600080fd5b506101b26004803603604081101561039657600080fd5b506001600160a01b038135169060200135151561087d565b6101b2600480360360808110156103c457600080fd5b6001600160a01b038235811692602081013590911691604082013591908101906080810160608201356401000000008111156103ff57600080fd5b82018360208201111561041157600080fd5b8035906020019184600183028401116401000000008311171561043357600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506108eb945050505050565b34801561048057600080fd5b5061049e6004803603602081101561049757600080fd5b5035610a5a565b6040805160208082528351818301528351919283929083019185019080838360005b838110156104d85781810151838201526020016104c0565b50505050905090810190601f1680156105055780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561051f57600080fd5b5061012c6004803603604081101561053657600080fd5b506001600160a01b0381358116916020013516610aa8565b60006001600160e01b031982166301ffc9a760e01b148061057f57506380ac58cd60e01b6001600160e01b03198316145b90505b919050565b6000805482106105d0576040805162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a59081d1bdad95b9259608a1b604482015290519081900360640190fd5b506000908152600460205260409020546001600160a01b031690565b6000818152600360205260409020546001600160a01b03163381148061063557506001600160a01b038116600090815260016020908152604080832033845290915290205460ff165b610674576040805162461bcd60e51b815260206004820152600b60248201526a139bdd08185b1b1bddd95960aa1b604482015290519081900360640190fd5b60008281526004602052604080822080546001600160a01b0319166001600160a01b0387811691821790925591518593918516917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a4505050565b60005481565b6106e1838383610ac8565b505050565b6001600160a01b038216600090815260026020526040812080548390811061070a57fe5b9060005260206000200154905092915050565b6002602052816000526040600020818154811061073657fe5b90600052602060002001600091509150505481565b6106e1838383604051806020016040528060008152506108eb565b6000805482106107ad576040805162461bcd60e51b815260206004820152600d60248201526c4f7574206f6620626f756e647360981b604482015290519081900360640190fd5b5090565b6000818152600360205260408120546001600160a01b03168061057f576040805162461bcd60e51b815260206004820152600860248201526727379037bbb732b960c11b604482015290519081900360640190fd5b600080549050610582826000610bc5565b60006001600160a01b038216610861576040805162461bcd60e51b815260206004820152600a602482015269273790181037bbb732b960b11b604482015290519081900360640190fd5b506001600160a01b031660009081526002602052604090205490565b3360008181526001602090815260408083206001600160a01b03871680855290835292819020805460ff1916861515908117909155815190815290519293927f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31929181900390910190a35050565b6108f6848484610ac8565b610908836001600160a01b0316610be1565b15610a5457604051630a85bd0160e11b80825233600483018181526001600160a01b0388811660248601526044850187905260806064860190815286516084870152865194959189169463150b7a0294938b938a938a93909160a40190602085019080838360005b83811015610988578181015183820152602001610970565b50505050905090810190601f1680156109b55780820380516001836020036101000a031916815260200191505b5095505050505050602060405180830381600087803b1580156109d757600080fd5b505af11580156109eb573d6000803e3d6000fd5b505050506040513d6020811015610a0157600080fd5b50516001600160e01b03191614610a54576040805162461bcd60e51b815260206004820152601260248201527157726f6e672072657475726e2076616c756560701b604482015290519081900360640190fd5b50505050565b60606000548210610a9f576040805162461bcd60e51b815260206004820152600a602482015269139bdd081b5a5b9d195960b21b604482015290519081900360640190fd5b61057f82610be7565b600160209081526000928352604080842090915290825290205460ff1681565b336001600160a01b0384161480610af557506000818152600460205260409020546001600160a01b031633145b80610b2357506001600160a01b038316600090815260016020908152604080832033845290915290205460ff165b610b6b576040805162461bcd60e51b8152602060048201526014602482015273151c985b9cd9995c881b9bdd08185b1b1bddd95960621b604482015290519081900360640190fd5b6001600160a01b038216610bb8576040805162461bcd60e51b815260206004820152600f60248201526e4e6f207a65726f206164647265737360881b604482015290519081900360640190fd5b6106e18184846000610bfa565b610bd460005460008484610bfa565b5050600080546001019055565b3b151590565b5060408051602081019091526000815290565b6000848152600360205260409020546001600160a01b039081169084168114610c5b576040805162461bcd60e51b815260206004820152600e60248201526d233937b6903737ba1037bbb732b960911b604482015290519081900360640190fd5b60006001600160a01b03851615610d7857506000858152600360209081526040808320546001600160a01b0388168452600290925282208054600160b81b830468ffffffffffffffffff169550600160a01b90920462ffffff169260001983019290919083908110610cc957fe5b906000526020600020015490508060026000896001600160a01b03166001600160a01b031681526020019081526020016000208462ffffff1681548110610d0c57fe5b600091825260208083209190910192909255828152600382526040808220805462ffffff60a01b1916600160a01b62ffffff8916021790556001600160a01b038a168252600290925220805480610d5f57fe5b6001900381819060005260206000200160009055905550505b506001600160a01b038084166000818152600260209081526040808320805460018101825590845282842081018b9055815160608101835285815262ffffff80831682860190815268ffffffffffffffffff808c168487019081528f8952600388528689209451855493519151909216600160b81b026001600160b81b0391909416600160a01b0262ffffff60a01b19928c166001600160a01b0319948516179290921691909117169190911790915560049093528184208054909316909255519093899392908916917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9190a450505050505056fea26469706673582212208b37a2e45a2fde41f60dbba03559ab5c06eea496b21595a80cd23339dac442f164736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": {}, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": {}, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 153, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "totalSupply", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 166, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "isApprovedForAll", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 171, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "tokensOf", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_array(t_uint256)dyn_storage)" + }, + { + "astId": 175, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "_tokens", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_struct(TokenInfo)160_storage)" + }, + { + "astId": 179, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "_approved", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_address)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)dyn_storage": { + "base": "t_uint256", + "encoding": "dynamic_array", + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_array(t_uint256)dyn_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256[])", + "numberOfBytes": "32", + "value": "t_array(t_uint256)dyn_storage" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_uint256,t_address)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_uint256,t_struct(TokenInfo)160_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct BoringMultipleNFT.TokenInfo)", + "numberOfBytes": "32", + "value": "t_struct(TokenInfo)160_storage" + }, + "t_struct(TokenInfo)160_storage": { + "encoding": "inplace", + "label": "struct BoringMultipleNFT.TokenInfo", + "members": [ + { + "astId": 155, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 157, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "index", + "offset": 20, + "slot": "0", + "type": "t_uint24" + }, + { + "astId": 159, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "data", + "offset": 23, + "slot": "0", + "type": "t_uint72" + } + ], + "numberOfBytes": "32" + }, + "t_uint24": { + "encoding": "inplace", + "label": "uint24", + "numberOfBytes": "3" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint72": { + "encoding": "inplace", + "label": "uint72", + "numberOfBytes": "9" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/ApesWethNFTPairMock.json b/deployments/ropsten/ApesWethNFTPairMock.json new file mode 100644 index 00000000..87f8997e --- /dev/null +++ b/deployments/ropsten/ApesWethNFTPairMock.json @@ -0,0 +1,4 @@ +{ + "address": "0x275f6306AE18a42BEf54Dc8cc82982077ab6695B", + "abi": [] +} \ No newline at end of file diff --git a/deployments/ropsten/BearsGuineasNFTPairMock.json b/deployments/ropsten/BearsGuineasNFTPairMock.json new file mode 100644 index 00000000..c87a167e --- /dev/null +++ b/deployments/ropsten/BearsGuineasNFTPairMock.json @@ -0,0 +1,4 @@ +{ + "address": "0x934073E597d395635E4303818625b6443A5030A4", + "abi": [] +} \ No newline at end of file diff --git a/deployments/ropsten/BearsNFTMock.json b/deployments/ropsten/BearsNFTMock.json new file mode 100644 index 00000000..6cbeffb9 --- /dev/null +++ b/deployments/ropsten/BearsNFTMock.json @@ -0,0 +1,582 @@ +{ + "address": "0x95d7c415bAAFf9446B457439F2fF269032A73fF6", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "approved", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceID", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokensOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } + ], + "transactionHash": "0xcf6d4d574ac2e97bad90c3ccc14b8fb39c338d3d403623d03987d46136eb160b", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0x95d7c415bAAFf9446B457439F2fF269032A73fF6", + "transactionIndex": 5, + "gasUsed": "864780", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x2556a798f9b6dd7098f8dbf9d3176e6e4ee31855e2093fdc807c9b953851421d", + "transactionHash": "0xcf6d4d574ac2e97bad90c3ccc14b8fb39c338d3d403623d03987d46136eb160b", + "logs": [], + "blockNumber": 12212440, + "cumulativeGasUsed": "1412174", + "status": 1, + "byzantium": true + }, + "args": [], + "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"ApprovalForAll\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"getApproved\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"approved\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"isApprovedForAll\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"mint\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"id\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"ownerOf\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"safeTransferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"setApprovalForAll\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes4\",\"name\":\"interfaceID\",\"type\":\"bytes4\"}],\"name\":\"supportsInterface\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"tokenByIndex\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"tokenOfOwnerByIndex\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"tokenURI\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokensOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/ERC721Mock.sol\":\"ERC721Mock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport \\\"./libraries/BoringAddress.sol\\\";\\nimport \\\"./libraries/BoringMath.sol\\\";\\nimport \\\"./interfaces/IERC721TokenReceiver.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nabstract contract BoringMultipleNFT {\\n /// This contract is an EIP-721 compliant contract with enumerable support\\n /// To optimize for gas, tokenId is sequential and start at 0. Also, tokens can't be removed/burned.\\n using BoringAddress for address;\\n using BoringMath for uint256;\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n uint256 public totalSupply = 0;\\n\\n struct TokenInfo {\\n address owner;\\n uint24 index; // index in the tokensOf array, one address can hold a maximum of 16,777,216 tokens\\n uint72 data; // data field can be usse to store traits\\n }\\n\\n // operator mappings as per usual\\n mapping(address => mapping(address => bool)) public isApprovedForAll;\\n mapping(address => uint256[]) public tokensOf; // Array of tokens owned by\\n mapping(uint256 => TokenInfo) internal _tokens; // The index in the tokensOf array for the token, needed to remove tokens from tokensOf\\n mapping(uint256 => address) internal _approved; // keep track of approved nft\\n\\n function supportsInterface(bytes4 interfaceID) external pure returns (bool) {\\n return\\n interfaceID == this.supportsInterface.selector || // EIP-165\\n interfaceID == 0x80ac58cd; // EIP-721\\n }\\n\\n function approve(address approved, uint256 tokenId) public payable {\\n address owner = _tokens[tokenId].owner;\\n require(msg.sender == owner || isApprovedForAll[owner][msg.sender], \\\"Not allowed\\\");\\n _approved[tokenId] = approved;\\n emit Approval(owner, approved, tokenId);\\n }\\n\\n function getApproved(uint256 tokenId) public view returns (address approved) {\\n require(tokenId < totalSupply, \\\"Invalid tokenId\\\");\\n return _approved[tokenId];\\n }\\n\\n function setApprovalForAll(address operator, bool approved) public {\\n isApprovedForAll[msg.sender][operator] = approved;\\n emit ApprovalForAll(msg.sender, operator, approved);\\n }\\n\\n function ownerOf(uint256 tokenId) public view returns (address) {\\n address owner = _tokens[tokenId].owner;\\n require(owner != address(0), \\\"No owner\\\");\\n return owner;\\n }\\n\\n function balanceOf(address owner) public view returns (uint256) {\\n require(owner != address(0), \\\"No 0 owner\\\");\\n return tokensOf[owner].length;\\n }\\n\\n function _transferBase(\\n uint256 tokenId,\\n address from,\\n address to,\\n uint72 data\\n ) internal {\\n address owner = _tokens[tokenId].owner;\\n require(from == owner, \\\"From not owner\\\");\\n\\n uint24 index;\\n // Remove the token from the current owner's tokensOf array\\n if (from != address(0)) {\\n index = _tokens[tokenId].index; // The index of the item to remove in the array\\n data = _tokens[tokenId].data;\\n uint256 last = tokensOf[from].length - 1;\\n uint256 lastTokenId = tokensOf[from][last];\\n tokensOf[from][index] = lastTokenId; // Copy the last item into the slot of the one to be removed\\n _tokens[lastTokenId].index = index; // Update the token index for the last item that was moved\\n tokensOf[from].pop(); // Delete the last item\\n }\\n\\n index = uint24(tokensOf[to].length);\\n tokensOf[to].push(tokenId);\\n _tokens[tokenId] = TokenInfo({owner: to, index: index, data: data});\\n\\n // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event.\\n _approved[tokenId] = address(0);\\n emit Transfer(from, to, tokenId);\\n }\\n\\n function _transfer(\\n address from,\\n address to,\\n uint256 tokenId\\n ) internal {\\n require(msg.sender == from || msg.sender == _approved[tokenId] || isApprovedForAll[from][msg.sender], \\\"Transfer not allowed\\\");\\n require(to != address(0), \\\"No zero address\\\");\\n // check for owner == from is in base\\n _transferBase(tokenId, from, to, 0);\\n }\\n\\n function transferFrom(\\n address from,\\n address to,\\n uint256 tokenId\\n ) public payable {\\n _transfer(from, to, tokenId);\\n }\\n\\n function safeTransferFrom(\\n address from,\\n address to,\\n uint256 tokenId\\n ) public payable {\\n safeTransferFrom(from, to, tokenId, \\\"\\\");\\n }\\n\\n function safeTransferFrom(\\n address from,\\n address to,\\n uint256 tokenId,\\n bytes memory data\\n ) public payable {\\n _transfer(from, to, tokenId);\\n if (to.isContract()) {\\n require(\\n IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) ==\\n bytes4(keccak256(\\\"onERC721Received(address,address,uint256,bytes)\\\")),\\n \\\"Wrong return value\\\"\\n );\\n }\\n }\\n\\n function tokenURI(uint256 tokenId) public view returns (string memory) {\\n require(tokenId < totalSupply, \\\"Not minted\\\");\\n return _tokenURI(tokenId);\\n }\\n\\n function _tokenURI(uint256 tokenId) internal view virtual returns (string memory);\\n\\n function tokenByIndex(uint256 index) public view returns (uint256) {\\n require(index < totalSupply, \\\"Out of bounds\\\");\\n return index; // This works due the optimization of sequential tokenIds and no burning\\n }\\n\\n function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) {\\n return tokensOf[owner][index];\\n }\\n\\n function _mint(address owner, uint72 data) internal {\\n _transferBase(totalSupply, address(0), owner, data);\\n totalSupply++;\\n }\\n}\\n\",\"keccak256\":\"0x2c90ee1540ed6bd1af2a98eba7aa99b8b528bd04ec558d1599fd1d71b4e667ca\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC721TokenReceiver.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC721TokenReceiver {\\n function onERC721Received(\\n address _operator,\\n address _from,\\n uint256 _tokenId,\\n bytes calldata _data\\n ) external returns (bytes4);\\n}\\n\",\"keccak256\":\"0x6dbac0edb2e4c5c806089a47a9819b21ca37f0eefe7a3b8cb4090b78f788c02b\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n\\n//SPDX-License-Identifier: MIT\\npragma solidity ^0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\nlibrary BoringAddress {\\n function isContract(address account) internal view returns (bool) {\\n uint256 size;\\n assembly {\\n size := extcodesize(account)\\n }\\n return size > 0;\\n }\\n}\\n\",\"keccak256\":\"0xcac0ac5a21ec520c48fb364204da3ae55460e46b0344d010583564d367deb030\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"contracts/mocks/ERC721Mock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\\\";\\n\\ncontract ERC721Mock is BoringMultipleNFT {\\n function mint(address owner) public returns (uint256 id) {\\n id = totalSupply;\\n _mint(owner, 0);\\n }\\n\\n function _tokenURI(uint256) internal view override returns (string memory) {\\n return \\\"\\\";\\n }\\n}\\n\",\"keccak256\":\"0xa99ad0ed4051c90f4ee7d6b0cef9a7f250cea5b7b519b9766696b297f2bc27fd\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60806040526000805534801561001457600080fd5b50610ea4806100246000396000f3fe6080604052600436106100f35760003560e01c80634f6ccce71161008a578063a22cb46511610059578063a22cb46514610373578063b88d4fde146103ae578063c87b56dd14610474578063e985e9c514610513576100f3565b80634f6ccce7146102b95780636352211e146102e35780636a6278421461030d57806370a0823114610340576100f3565b806323b872dd116100c657806323b872dd146101db5780632f745c591461021157806338a3dbae1461024a57806342842e0e14610283576100f3565b806301ffc9a7146100f8578063081812fc14610140578063095ea7b31461018657806318160ddd146101b4575b600080fd5b34801561010457600080fd5b5061012c6004803603602081101561011b57600080fd5b50356001600160e01b03191661054e565b604080519115158252519081900360200190f35b34801561014c57600080fd5b5061016a6004803603602081101561016357600080fd5b5035610587565b604080516001600160a01b039092168252519081900360200190f35b6101b26004803603604081101561019c57600080fd5b506001600160a01b0381351690602001356105ec565b005b3480156101c057600080fd5b506101c96106d0565b60408051918252519081900360200190f35b6101b2600480360360608110156101f157600080fd5b506001600160a01b038135811691602081013590911690604001356106d6565b34801561021d57600080fd5b506101c96004803603604081101561023457600080fd5b506001600160a01b0381351690602001356106e6565b34801561025657600080fd5b506101c96004803603604081101561026d57600080fd5b506001600160a01b03813516906020013561071d565b6101b26004803603606081101561029957600080fd5b506001600160a01b0381358116916020810135909116906040013561074b565b3480156102c557600080fd5b506101c9600480360360208110156102dc57600080fd5b5035610766565b3480156102ef57600080fd5b5061016a6004803603602081101561030657600080fd5b50356107b1565b34801561031957600080fd5b506101c96004803603602081101561033057600080fd5b50356001600160a01b0316610806565b34801561034c57600080fd5b506101c96004803603602081101561036357600080fd5b50356001600160a01b0316610817565b34801561037f57600080fd5b506101b26004803603604081101561039657600080fd5b506001600160a01b038135169060200135151561087d565b6101b2600480360360808110156103c457600080fd5b6001600160a01b038235811692602081013590911691604082013591908101906080810160608201356401000000008111156103ff57600080fd5b82018360208201111561041157600080fd5b8035906020019184600183028401116401000000008311171561043357600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506108eb945050505050565b34801561048057600080fd5b5061049e6004803603602081101561049757600080fd5b5035610a5a565b6040805160208082528351818301528351919283929083019185019080838360005b838110156104d85781810151838201526020016104c0565b50505050905090810190601f1680156105055780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561051f57600080fd5b5061012c6004803603604081101561053657600080fd5b506001600160a01b0381358116916020013516610aa8565b60006001600160e01b031982166301ffc9a760e01b148061057f57506380ac58cd60e01b6001600160e01b03198316145b90505b919050565b6000805482106105d0576040805162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a59081d1bdad95b9259608a1b604482015290519081900360640190fd5b506000908152600460205260409020546001600160a01b031690565b6000818152600360205260409020546001600160a01b03163381148061063557506001600160a01b038116600090815260016020908152604080832033845290915290205460ff165b610674576040805162461bcd60e51b815260206004820152600b60248201526a139bdd08185b1b1bddd95960aa1b604482015290519081900360640190fd5b60008281526004602052604080822080546001600160a01b0319166001600160a01b0387811691821790925591518593918516917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a4505050565b60005481565b6106e1838383610ac8565b505050565b6001600160a01b038216600090815260026020526040812080548390811061070a57fe5b9060005260206000200154905092915050565b6002602052816000526040600020818154811061073657fe5b90600052602060002001600091509150505481565b6106e1838383604051806020016040528060008152506108eb565b6000805482106107ad576040805162461bcd60e51b815260206004820152600d60248201526c4f7574206f6620626f756e647360981b604482015290519081900360640190fd5b5090565b6000818152600360205260408120546001600160a01b03168061057f576040805162461bcd60e51b815260206004820152600860248201526727379037bbb732b960c11b604482015290519081900360640190fd5b600080549050610582826000610bc5565b60006001600160a01b038216610861576040805162461bcd60e51b815260206004820152600a602482015269273790181037bbb732b960b11b604482015290519081900360640190fd5b506001600160a01b031660009081526002602052604090205490565b3360008181526001602090815260408083206001600160a01b03871680855290835292819020805460ff1916861515908117909155815190815290519293927f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31929181900390910190a35050565b6108f6848484610ac8565b610908836001600160a01b0316610be1565b15610a5457604051630a85bd0160e11b80825233600483018181526001600160a01b0388811660248601526044850187905260806064860190815286516084870152865194959189169463150b7a0294938b938a938a93909160a40190602085019080838360005b83811015610988578181015183820152602001610970565b50505050905090810190601f1680156109b55780820380516001836020036101000a031916815260200191505b5095505050505050602060405180830381600087803b1580156109d757600080fd5b505af11580156109eb573d6000803e3d6000fd5b505050506040513d6020811015610a0157600080fd5b50516001600160e01b03191614610a54576040805162461bcd60e51b815260206004820152601260248201527157726f6e672072657475726e2076616c756560701b604482015290519081900360640190fd5b50505050565b60606000548210610a9f576040805162461bcd60e51b815260206004820152600a602482015269139bdd081b5a5b9d195960b21b604482015290519081900360640190fd5b61057f82610be7565b600160209081526000928352604080842090915290825290205460ff1681565b336001600160a01b0384161480610af557506000818152600460205260409020546001600160a01b031633145b80610b2357506001600160a01b038316600090815260016020908152604080832033845290915290205460ff165b610b6b576040805162461bcd60e51b8152602060048201526014602482015273151c985b9cd9995c881b9bdd08185b1b1bddd95960621b604482015290519081900360640190fd5b6001600160a01b038216610bb8576040805162461bcd60e51b815260206004820152600f60248201526e4e6f207a65726f206164647265737360881b604482015290519081900360640190fd5b6106e18184846000610bfa565b610bd460005460008484610bfa565b5050600080546001019055565b3b151590565b5060408051602081019091526000815290565b6000848152600360205260409020546001600160a01b039081169084168114610c5b576040805162461bcd60e51b815260206004820152600e60248201526d233937b6903737ba1037bbb732b960911b604482015290519081900360640190fd5b60006001600160a01b03851615610d7857506000858152600360209081526040808320546001600160a01b0388168452600290925282208054600160b81b830468ffffffffffffffffff169550600160a01b90920462ffffff169260001983019290919083908110610cc957fe5b906000526020600020015490508060026000896001600160a01b03166001600160a01b031681526020019081526020016000208462ffffff1681548110610d0c57fe5b600091825260208083209190910192909255828152600382526040808220805462ffffff60a01b1916600160a01b62ffffff8916021790556001600160a01b038a168252600290925220805480610d5f57fe5b6001900381819060005260206000200160009055905550505b506001600160a01b038084166000818152600260209081526040808320805460018101825590845282842081018b9055815160608101835285815262ffffff80831682860190815268ffffffffffffffffff808c168487019081528f8952600388528689209451855493519151909216600160b81b026001600160b81b0391909416600160a01b0262ffffff60a01b19928c166001600160a01b0319948516179290921691909117169190911790915560049093528184208054909316909255519093899392908916917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9190a450505050505056fea26469706673582212208b37a2e45a2fde41f60dbba03559ab5c06eea496b21595a80cd23339dac442f164736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106100f35760003560e01c80634f6ccce71161008a578063a22cb46511610059578063a22cb46514610373578063b88d4fde146103ae578063c87b56dd14610474578063e985e9c514610513576100f3565b80634f6ccce7146102b95780636352211e146102e35780636a6278421461030d57806370a0823114610340576100f3565b806323b872dd116100c657806323b872dd146101db5780632f745c591461021157806338a3dbae1461024a57806342842e0e14610283576100f3565b806301ffc9a7146100f8578063081812fc14610140578063095ea7b31461018657806318160ddd146101b4575b600080fd5b34801561010457600080fd5b5061012c6004803603602081101561011b57600080fd5b50356001600160e01b03191661054e565b604080519115158252519081900360200190f35b34801561014c57600080fd5b5061016a6004803603602081101561016357600080fd5b5035610587565b604080516001600160a01b039092168252519081900360200190f35b6101b26004803603604081101561019c57600080fd5b506001600160a01b0381351690602001356105ec565b005b3480156101c057600080fd5b506101c96106d0565b60408051918252519081900360200190f35b6101b2600480360360608110156101f157600080fd5b506001600160a01b038135811691602081013590911690604001356106d6565b34801561021d57600080fd5b506101c96004803603604081101561023457600080fd5b506001600160a01b0381351690602001356106e6565b34801561025657600080fd5b506101c96004803603604081101561026d57600080fd5b506001600160a01b03813516906020013561071d565b6101b26004803603606081101561029957600080fd5b506001600160a01b0381358116916020810135909116906040013561074b565b3480156102c557600080fd5b506101c9600480360360208110156102dc57600080fd5b5035610766565b3480156102ef57600080fd5b5061016a6004803603602081101561030657600080fd5b50356107b1565b34801561031957600080fd5b506101c96004803603602081101561033057600080fd5b50356001600160a01b0316610806565b34801561034c57600080fd5b506101c96004803603602081101561036357600080fd5b50356001600160a01b0316610817565b34801561037f57600080fd5b506101b26004803603604081101561039657600080fd5b506001600160a01b038135169060200135151561087d565b6101b2600480360360808110156103c457600080fd5b6001600160a01b038235811692602081013590911691604082013591908101906080810160608201356401000000008111156103ff57600080fd5b82018360208201111561041157600080fd5b8035906020019184600183028401116401000000008311171561043357600080fd5b91908080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152509295506108eb945050505050565b34801561048057600080fd5b5061049e6004803603602081101561049757600080fd5b5035610a5a565b6040805160208082528351818301528351919283929083019185019080838360005b838110156104d85781810151838201526020016104c0565b50505050905090810190601f1680156105055780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561051f57600080fd5b5061012c6004803603604081101561053657600080fd5b506001600160a01b0381358116916020013516610aa8565b60006001600160e01b031982166301ffc9a760e01b148061057f57506380ac58cd60e01b6001600160e01b03198316145b90505b919050565b6000805482106105d0576040805162461bcd60e51b815260206004820152600f60248201526e125b9d985b1a59081d1bdad95b9259608a1b604482015290519081900360640190fd5b506000908152600460205260409020546001600160a01b031690565b6000818152600360205260409020546001600160a01b03163381148061063557506001600160a01b038116600090815260016020908152604080832033845290915290205460ff165b610674576040805162461bcd60e51b815260206004820152600b60248201526a139bdd08185b1b1bddd95960aa1b604482015290519081900360640190fd5b60008281526004602052604080822080546001600160a01b0319166001600160a01b0387811691821790925591518593918516917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591a4505050565b60005481565b6106e1838383610ac8565b505050565b6001600160a01b038216600090815260026020526040812080548390811061070a57fe5b9060005260206000200154905092915050565b6002602052816000526040600020818154811061073657fe5b90600052602060002001600091509150505481565b6106e1838383604051806020016040528060008152506108eb565b6000805482106107ad576040805162461bcd60e51b815260206004820152600d60248201526c4f7574206f6620626f756e647360981b604482015290519081900360640190fd5b5090565b6000818152600360205260408120546001600160a01b03168061057f576040805162461bcd60e51b815260206004820152600860248201526727379037bbb732b960c11b604482015290519081900360640190fd5b600080549050610582826000610bc5565b60006001600160a01b038216610861576040805162461bcd60e51b815260206004820152600a602482015269273790181037bbb732b960b11b604482015290519081900360640190fd5b506001600160a01b031660009081526002602052604090205490565b3360008181526001602090815260408083206001600160a01b03871680855290835292819020805460ff1916861515908117909155815190815290519293927f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31929181900390910190a35050565b6108f6848484610ac8565b610908836001600160a01b0316610be1565b15610a5457604051630a85bd0160e11b80825233600483018181526001600160a01b0388811660248601526044850187905260806064860190815286516084870152865194959189169463150b7a0294938b938a938a93909160a40190602085019080838360005b83811015610988578181015183820152602001610970565b50505050905090810190601f1680156109b55780820380516001836020036101000a031916815260200191505b5095505050505050602060405180830381600087803b1580156109d757600080fd5b505af11580156109eb573d6000803e3d6000fd5b505050506040513d6020811015610a0157600080fd5b50516001600160e01b03191614610a54576040805162461bcd60e51b815260206004820152601260248201527157726f6e672072657475726e2076616c756560701b604482015290519081900360640190fd5b50505050565b60606000548210610a9f576040805162461bcd60e51b815260206004820152600a602482015269139bdd081b5a5b9d195960b21b604482015290519081900360640190fd5b61057f82610be7565b600160209081526000928352604080842090915290825290205460ff1681565b336001600160a01b0384161480610af557506000818152600460205260409020546001600160a01b031633145b80610b2357506001600160a01b038316600090815260016020908152604080832033845290915290205460ff165b610b6b576040805162461bcd60e51b8152602060048201526014602482015273151c985b9cd9995c881b9bdd08185b1b1bddd95960621b604482015290519081900360640190fd5b6001600160a01b038216610bb8576040805162461bcd60e51b815260206004820152600f60248201526e4e6f207a65726f206164647265737360881b604482015290519081900360640190fd5b6106e18184846000610bfa565b610bd460005460008484610bfa565b5050600080546001019055565b3b151590565b5060408051602081019091526000815290565b6000848152600360205260409020546001600160a01b039081169084168114610c5b576040805162461bcd60e51b815260206004820152600e60248201526d233937b6903737ba1037bbb732b960911b604482015290519081900360640190fd5b60006001600160a01b03851615610d7857506000858152600360209081526040808320546001600160a01b0388168452600290925282208054600160b81b830468ffffffffffffffffff169550600160a01b90920462ffffff169260001983019290919083908110610cc957fe5b906000526020600020015490508060026000896001600160a01b03166001600160a01b031681526020019081526020016000208462ffffff1681548110610d0c57fe5b600091825260208083209190910192909255828152600382526040808220805462ffffff60a01b1916600160a01b62ffffff8916021790556001600160a01b038a168252600290925220805480610d5f57fe5b6001900381819060005260206000200160009055905550505b506001600160a01b038084166000818152600260209081526040808320805460018101825590845282842081018b9055815160608101835285815262ffffff80831682860190815268ffffffffffffffffff808c168487019081528f8952600388528689209451855493519151909216600160b81b026001600160b81b0391909416600160a01b0262ffffff60a01b19928c166001600160a01b0319948516179290921691909117169190911790915560049093528184208054909316909255519093899392908916917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9190a450505050505056fea26469706673582212208b37a2e45a2fde41f60dbba03559ab5c06eea496b21595a80cd23339dac442f164736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": {}, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": {}, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 153, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "totalSupply", + "offset": 0, + "slot": "0", + "type": "t_uint256" + }, + { + "astId": 166, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "isApprovedForAll", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 171, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "tokensOf", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_array(t_uint256)dyn_storage)" + }, + { + "astId": 175, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "_tokens", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_struct(TokenInfo)160_storage)" + }, + { + "astId": 179, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "_approved", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_address)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)dyn_storage": { + "base": "t_uint256", + "encoding": "dynamic_array", + "label": "uint256[]", + "numberOfBytes": "32" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_array(t_uint256)dyn_storage)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256[])", + "numberOfBytes": "32", + "value": "t_array(t_uint256)dyn_storage" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_uint256,t_address)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_uint256,t_struct(TokenInfo)160_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct BoringMultipleNFT.TokenInfo)", + "numberOfBytes": "32", + "value": "t_struct(TokenInfo)160_storage" + }, + "t_struct(TokenInfo)160_storage": { + "encoding": "inplace", + "label": "struct BoringMultipleNFT.TokenInfo", + "members": [ + { + "astId": 155, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 157, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "index", + "offset": 20, + "slot": "0", + "type": "t_uint24" + }, + { + "astId": 159, + "contract": "contracts/mocks/ERC721Mock.sol:ERC721Mock", + "label": "data", + "offset": 23, + "slot": "0", + "type": "t_uint72" + } + ], + "numberOfBytes": "32" + }, + "t_uint24": { + "encoding": "inplace", + "label": "uint24", + "numberOfBytes": "3" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint72": { + "encoding": "inplace", + "label": "uint72", + "numberOfBytes": "9" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/BentoBoxMock.json b/deployments/ropsten/BentoBoxMock.json new file mode 100644 index 00000000..c350e924 --- /dev/null +++ b/deployments/ropsten/BentoBoxMock.json @@ -0,0 +1,1681 @@ +{ + "address": "0x9A5620779feF1928eF87c1111491212efC2C3cB8", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "weth", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": true, + "internalType": "address", + "name": "cloneAddress", + "type": "address" + } + ], + "name": "LogDeploy", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "LogDeposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "LogFlashLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "protocol", + "type": "address" + } + ], + "name": "LogRegisterProtocol", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "LogSetMasterContractApproval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LogStrategyDivest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LogStrategyInvest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LogStrategyLoss", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LogStrategyProfit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IStrategy", + "name": "strategy", + "type": "address" + } + ], + "name": "LogStrategyQueued", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "contract IStrategy", + "name": "strategy", + "type": "address" + } + ], + "name": "LogStrategySet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "targetPercentage", + "type": "uint256" + } + ], + "name": "LogStrategyTargetPercentage", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "LogTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "LogWhiteListMasterContract", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "LogWithdraw", + "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": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addProfit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "calls", + "type": "bytes[]" + }, + { + "internalType": "bool", + "name": "revertOnFail", + "type": "bool" + } + ], + "name": "batch", + "outputs": [ + { + "internalType": "bool[]", + "name": "successes", + "type": "bool[]" + }, + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IBatchFlashBorrower", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address[]", + "name": "receivers", + "type": "address[]" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "batchFlashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "claimOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "useCreate2", + "type": "bool" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "cloneAddress", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token_", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shareOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IFlashBorrower", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "bool", + "name": "balance", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "maxChangeAmount", + "type": "uint256" + } + ], + "name": "harvest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "masterContractApproved", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "masterContractOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "pendingStrategy", + "outputs": [ + { + "internalType": "contract IStrategy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permitToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "registerProtocol", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "setMasterContractApproval", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "contract IStrategy", + "name": "newStrategy", + "type": "address" + } + ], + "name": "setStrategy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint64", + "name": "targetPercentage_", + "type": "uint64" + } + ], + "name": "setStrategyTargetPercentage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "strategy", + "outputs": [ + { + "internalType": "contract IStrategy", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "strategyData", + "outputs": [ + { + "internalType": "uint64", + "name": "strategyStartDate", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "targetPercentage", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "balance", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "takeLoss", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "share", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "roundUp", + "type": "bool" + } + ], + "name": "toAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "roundUp", + "type": "bool" + } + ], + "name": "toShare", + "outputs": [ + { + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "name": "totals", + "outputs": [ + { + "internalType": "uint128", + "name": "elastic", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "base", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address[]", + "name": "tos", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "shares", + "type": "uint256[]" + } + ], + "name": "transferMultiple", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + }, + { + "internalType": "bool", + "name": "direct", + "type": "bool" + }, + { + "internalType": "bool", + "name": "renounce", + "type": "bool" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "masterContract", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "whitelistMasterContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "whitelistedMasterContracts", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token_", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "share", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shareOut", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "transactionHash": "0x9760e459f1570c458a46d5c865f512bdc206956bcca554cc9feb7d086b39d9a7", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0x9A5620779feF1928eF87c1111491212efC2C3cB8", + "transactionIndex": 11, + "gasUsed": "4012777", + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000001000000000002000000000000000000000000020000000000000000000800000000000002000000000000000000400000000000000000000000000000000000000000000000000000000000000200000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000010000000", + "blockHash": "0xda592ac19821d8153f1dfcf90c64f76e095cba2e2364a06d5bcf157689ff177e", + "transactionHash": "0x9760e459f1570c458a46d5c865f512bdc206956bcca554cc9feb7d086b39d9a7", + "logs": [ + { + "transactionIndex": 11, + "blockNumber": 12212437, + "transactionHash": "0x9760e459f1570c458a46d5c865f512bdc206956bcca554cc9feb7d086b39d9a7", + "address": "0x9A5620779feF1928eF87c1111491212efC2C3cB8", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000063a1e3877b1662a9ad124f8611b06e3ffbc29cba" + ], + "data": "0x", + "logIndex": 5, + "blockHash": "0xda592ac19821d8153f1dfcf90c64f76e095cba2e2364a06d5bcf157689ff177e" + } + ], + "blockNumber": 12212437, + "cumulativeGasUsed": "4594540", + "status": 1, + "byzantium": true + }, + "args": [ + "0xf34200FB29d202867DbE147696e6c5650F81Fbb3" + ], + "solcInputHash": "aca01c1a6fd2699b1adc885469d39e7b", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"weth\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"cloneAddress\",\"type\":\"address\"}],\"name\":\"LogDeploy\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"LogDeposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeAmount\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"}],\"name\":\"LogFlashLoan\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"protocol\",\"type\":\"address\"}],\"name\":\"LogRegisterProtocol\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"LogSetMasterContractApproval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"LogStrategyDivest\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"LogStrategyInvest\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"LogStrategyLoss\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"LogStrategyProfit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"LogStrategyQueued\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"contract IStrategy\",\"name\":\"strategy\",\"type\":\"address\"}],\"name\":\"LogStrategySet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"targetPercentage\",\"type\":\"uint256\"}],\"name\":\"LogStrategyTargetPercentage\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"LogTransfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"LogWhiteListMasterContract\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"LogWithdraw\",\"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\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"addProfit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes[]\",\"name\":\"calls\",\"type\":\"bytes[]\"},{\"internalType\":\"bool\",\"name\":\"revertOnFail\",\"type\":\"bool\"}],\"name\":\"batch\",\"outputs\":[{\"internalType\":\"bool[]\",\"name\":\"successes\",\"type\":\"bool[]\"},{\"internalType\":\"bytes[]\",\"name\":\"results\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IBatchFlashBorrower\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address[]\",\"name\":\"receivers\",\"type\":\"address[]\"},{\"internalType\":\"contract IERC20[]\",\"name\":\"tokens\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"amounts\",\"type\":\"uint256[]\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"batchFlashLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"claimOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"bool\",\"name\":\"useCreate2\",\"type\":\"bool\"}],\"name\":\"deploy\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"cloneAddress\",\"type\":\"address\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amountOut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"shareOut\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IFlashBorrower\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"receiver\",\"type\":\"address\"},{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"flashLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"balance\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"maxChangeAmount\",\"type\":\"uint256\"}],\"name\":\"harvest\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"masterContractApproved\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"masterContractOf\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"pendingStrategy\",\"outputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"permitToken\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"registerProtocol\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"setMasterContractApproval\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"contract IStrategy\",\"name\":\"newStrategy\",\"type\":\"address\"}],\"name\":\"setStrategy\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"targetPercentage_\",\"type\":\"uint64\"}],\"name\":\"setStrategyTargetPercentage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"strategy\",\"outputs\":[{\"internalType\":\"contract IStrategy\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"strategyData\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"strategyStartDate\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"targetPercentage\",\"type\":\"uint64\"},{\"internalType\":\"uint128\",\"name\":\"balance\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"takeLoss\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"roundUp\",\"type\":\"bool\"}],\"name\":\"toAmount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"roundUp\",\"type\":\"bool\"}],\"name\":\"toShare\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"totals\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"elastic\",\"type\":\"uint128\"},{\"internalType\":\"uint128\",\"name\":\"base\",\"type\":\"uint128\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address[]\",\"name\":\"tos\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"shares\",\"type\":\"uint256[]\"}],\"name\":\"transferMultiple\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"direct\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"renounce\",\"type\":\"bool\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"masterContract\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"approved\",\"type\":\"bool\"}],\"name\":\"whitelistMasterContract\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"whitelistedMasterContracts\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"token_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"share\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amountOut\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"shareOut\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"batch(bytes[],bool)\":{\"params\":{\"calls\":\"An array of inputs for each call.\",\"revertOnFail\":\"If True then reverts after a failed call and stops doing further calls.\"},\"returns\":{\"results\":\"An array with the returned data of each function call, mapped one-to-one to `calls`.\",\"successes\":\"An array indicating the success of a call, mapped one-to-one to `calls`.\"}},\"batchFlashLoan(address,address[],address[],uint256[],bytes)\":{\"params\":{\"amounts\":\"of the tokens for each receiver.\",\"borrower\":\"The address of the contract that implements and conforms to `IBatchFlashBorrower` and handles the flashloan.\",\"data\":\"The calldata to pass to the `borrower` contract.\",\"receivers\":\"An array of the token receivers. A one-to-one mapping with `tokens` and `amounts`.\",\"tokens\":\"The addresses of the tokens.\"}},\"deploy(address,bytes,bool)\":{\"params\":{\"data\":\"Additional abi encoded calldata that is passed to the new clone via `IMasterContract.init`.\",\"masterContract\":\"The address of the contract to clone.\",\"useCreate2\":\"Creates the clone by using the CREATE2 opcode, in this case `data` will be used as salt.\"},\"returns\":{\"cloneAddress\":\"Address of the created clone contract.\"}},\"deposit(address,address,address,uint256,uint256)\":{\"params\":{\"amount\":\"Token amount in native representation to deposit.\",\"from\":\"which account to pull the tokens.\",\"share\":\"Token amount represented in shares to deposit. Takes precedence over `amount`.\",\"to\":\"which account to push the tokens.\",\"token_\":\"The ERC-20 token to deposit.\"},\"returns\":{\"amountOut\":\"The amount deposited.\",\"shareOut\":\"The deposited amount repesented in shares.\"}},\"flashLoan(address,address,address,uint256,bytes)\":{\"params\":{\"amount\":\"of the tokens to receive.\",\"borrower\":\"The address of the contract that implements and conforms to `IFlashBorrower` and handles the flashloan.\",\"data\":\"The calldata to pass to the `borrower` contract.\",\"receiver\":\"Address of the token receiver.\",\"token\":\"The address of the token to receive.\"}},\"harvest(address,bool,uint256)\":{\"params\":{\"balance\":\"True if housekeeping should be done.\",\"maxChangeAmount\":\"The maximum amount for either pulling or pushing from/to the `IStrategy` contract.\",\"token\":\"The address of the token for which a strategy is deployed.\"}},\"setMasterContractApproval(address,address,bool,uint8,bytes32,bytes32)\":{\"params\":{\"approved\":\"If True approves access. If False revokes access.\",\"masterContract\":\"The address who gains or loses access.\",\"r\":\"Part of the signature. (See EIP-191)\",\"s\":\"Part of the signature. (See EIP-191)\",\"user\":\"The address of the user that approves or revokes access.\",\"v\":\"Part of the signature. (See EIP-191)\"}},\"setStrategy(address,address)\":{\"details\":\"Only the owner of this contract is allowed to change this.\",\"params\":{\"newStrategy\":\"The address of the contract that conforms to `IStrategy`.\",\"token\":\"The address of the token that maps to a strategy to change.\"}},\"setStrategyTargetPercentage(address,uint64)\":{\"details\":\"Only the owner of this contract is allowed to change this.\",\"params\":{\"targetPercentage_\":\"The new target in percent. Must be lesser or equal to `MAX_TARGET_PERCENTAGE`.\",\"token\":\"The address of the token that maps to a strategy to change.\"}},\"toAmount(address,uint256,bool)\":{\"details\":\"Helper function represent shares back into the `token` amount.\",\"params\":{\"roundUp\":\"If the result should be rounded up.\",\"share\":\"The amount of shares.\",\"token\":\"The ERC-20 token.\"},\"returns\":{\"amount\":\"The share amount back into native representation.\"}},\"toShare(address,uint256,bool)\":{\"details\":\"Helper function to represent an `amount` of `token` in shares.\",\"params\":{\"amount\":\"The `token` amount.\",\"roundUp\":\"If the result `share` should be rounded up.\",\"token\":\"The ERC-20 token.\"},\"returns\":{\"share\":\"The token amount represented in shares.\"}},\"transfer(address,address,address,uint256)\":{\"params\":{\"from\":\"which user to pull the tokens.\",\"share\":\"The amount of `token` in shares.\",\"to\":\"which user to push the tokens.\",\"token\":\"The ERC-20 token to transfer.\"}},\"transferMultiple(address,address,address[],uint256[])\":{\"params\":{\"from\":\"which user to pull the tokens.\",\"shares\":\"The amount of `token` in shares for each receiver in `tos`.\",\"token\":\"The ERC-20 token to transfer.\",\"tos\":\"The receivers of the tokens.\"}},\"transferOwnership(address,bool,bool)\":{\"params\":{\"direct\":\"True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\",\"newOwner\":\"Address of the new owner.\",\"renounce\":\"Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\"}},\"withdraw(address,address,address,uint256,uint256)\":{\"params\":{\"amount\":\"of tokens. Either one of `amount` or `share` needs to be supplied.\",\"from\":\"which user to pull the tokens.\",\"share\":\"Like above, but `share` takes precedence over `amount`.\",\"to\":\"which user to push the tokens.\",\"token_\":\"The ERC-20 token to withdraw.\"}}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"batch(bytes[],bool)\":{\"notice\":\"Allows batched call to self (this contract).\"},\"batchFlashLoan(address,address[],address[],uint256[],bytes)\":{\"notice\":\"Support for batched flashloans. Useful to request multiple different `tokens` in a single transaction.\"},\"claimOwnership()\":{\"notice\":\"Needs to be called by `pendingOwner` to claim ownership.\"},\"deploy(address,bytes,bool)\":{\"notice\":\"Deploys a given master Contract as a clone. Any ETH transferred with this call is forwarded to the new clone. Emits `LogDeploy`.\"},\"deposit(address,address,address,uint256,uint256)\":{\"notice\":\"Deposit an amount of `token` represented in either `amount` or `share`.\"},\"flashLoan(address,address,address,uint256,bytes)\":{\"notice\":\"Flashloan ability.\"},\"harvest(address,bool,uint256)\":{\"notice\":\"The actual process of yield farming. Executes the strategy of `token`. Optionally does housekeeping if `balance` is true. `maxChangeAmount` is relevant for skimming or withdrawing if `balance` is true.\"},\"masterContractApproved(address,address)\":{\"notice\":\"masterContract to user to approval state\"},\"masterContractOf(address)\":{\"notice\":\"Mapping from clone contracts to their masterContract.\"},\"nonces(address)\":{\"notice\":\"user nonces for masterContract approvals\"},\"permitToken(address,address,address,uint256,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Call wrapper that performs `ERC20.permit` on `token`. Lookup `IERC20.permit`.\"},\"registerProtocol()\":{\"notice\":\"Other contracts need to register with this master contract so that users can approve them for the BentoBox.\"},\"setMasterContractApproval(address,address,bool,uint8,bytes32,bytes32)\":{\"notice\":\"Approves or revokes a `masterContract` access to `user` funds.\"},\"setStrategy(address,address)\":{\"notice\":\"Sets the contract address of a new strategy that conforms to `IStrategy` for `token`. Must be called twice with the same arguments. A new strategy becomes pending first and can be activated once `STRATEGY_DELAY` is over.\"},\"setStrategyTargetPercentage(address,uint64)\":{\"notice\":\"Sets the target percentage of the strategy for `token`.\"},\"transfer(address,address,address,uint256)\":{\"notice\":\"Transfer shares from a user account to another one.\"},\"transferMultiple(address,address,address[],uint256[])\":{\"notice\":\"Transfer shares from a user account to multiple other ones.\"},\"transferOwnership(address,bool,bool)\":{\"notice\":\"Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`.\"},\"whitelistMasterContract(address,bool)\":{\"notice\":\"Enables or disables a contract for approval without signed message.\"},\"whitelistedMasterContracts(address)\":{\"notice\":\"masterContract to whitelisted state for approval without signed message\"},\"withdraw(address,address,address,uint256,uint256)\":{\"notice\":\"Withdraws an amount of `token` from a user account.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/BentoBoxMock.sol\":\"BentoBoxMock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"contracts/BentoBoxFlat.sol\":{\"content\":\"// SPDX-License-Identifier: UNLICENSED\\n// The BentoBox\\n\\n// \\u2584\\u2584\\u2584\\u2584\\u00b7 \\u2584\\u2584\\u2584 . \\u2590 \\u2584 \\u2584\\u2584\\u2584\\u2584\\u2584 \\u2584\\u2584\\u2584\\u2584\\u00b7 \\u2590\\u2584\\u2022 \\u2584\\n// \\u2590\\u2588 \\u2580\\u2588\\u25aa\\u2580\\u2584.\\u2580\\u00b7\\u2588\\u258c\\u2590\\u2588\\u2022\\u2588\\u2588 \\u25aa \\u2590\\u2588 \\u2580\\u2588\\u25aa\\u25aa \\u2588\\u258c\\u2588\\u258c\\u25aa\\n// \\u2590\\u2588\\u2580\\u2580\\u2588\\u2584\\u2590\\u2580\\u2580\\u25aa\\u2584\\u2590\\u2588\\u2590\\u2590\\u258c \\u2590\\u2588.\\u25aa \\u2584\\u2588\\u2580\\u2584 \\u2590\\u2588\\u2580\\u2580\\u2588\\u2584 \\u2584\\u2588\\u2580\\u2584 \\u00b7\\u2588\\u2588\\u00b7\\n// \\u2588\\u2588\\u2584\\u25aa\\u2590\\u2588\\u2590\\u2588\\u2584\\u2584\\u258c\\u2588\\u2588\\u2590\\u2588\\u258c \\u2590\\u2588\\u258c\\u00b7\\u2590\\u2588\\u258c.\\u2590\\u258c\\u2588\\u2588\\u2584\\u25aa\\u2590\\u2588\\u2590\\u2588\\u258c.\\u2590\\u258c\\u25aa\\u2590\\u2588\\u00b7\\u2588\\u258c\\n// \\u00b7\\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580 \\u2580\\u2580 \\u2588\\u25aa \\u2580\\u2580\\u2580 \\u2580\\u2588\\u2584\\u2580\\u25aa\\u00b7\\u2580\\u2580\\u2580\\u2580 \\u2580\\u2588\\u2584\\u2580\\u25aa\\u2022\\u2580\\u2580 \\u2580\\u2580\\n\\n// This contract stores funds, handles their transfers, supports flash loans and strategies.\\n\\n// Copyright (c) 2021 BoringCrypto - All rights reserved\\n// Twitter: @Boring_Crypto\\n\\n// Special thanks to Keno for all his hard work and support\\n\\n// Version 22-Mar-2021\\n\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\n\\n// solhint-disable avoid-low-level-calls\\n// solhint-disable not-rely-on-time\\n// solhint-disable no-inline-assembly\\n\\n// File @boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol@v1.2.0\\n// License-Identifier: MIT\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\\n// File contracts/interfaces/IFlashLoan.sol\\n// License-Identifier: MIT\\n\\ninterface IFlashBorrower {\\n /// @notice The flashloan callback. `amount` + `fee` needs to repayed to msg.sender before this call returns.\\n /// @param sender The address of the invoker of this flashloan.\\n /// @param token The address of the token that is loaned.\\n /// @param amount of the `token` that is loaned.\\n /// @param fee The fee that needs to be paid on top for this loan. Needs to be the same as `token`.\\n /// @param data Additional data that was passed to the flashloan function.\\n function onFlashLoan(\\n address sender,\\n IERC20 token,\\n uint256 amount,\\n uint256 fee,\\n bytes calldata data\\n ) external;\\n}\\n\\ninterface IBatchFlashBorrower {\\n /// @notice The callback for batched flashloans. Every amount + fee needs to repayed to msg.sender before this call returns.\\n /// @param sender The address of the invoker of this flashloan.\\n /// @param tokens Array of addresses for ERC-20 tokens that is loaned.\\n /// @param amounts A one-to-one map to `tokens` that is loaned.\\n /// @param fees A one-to-one map to `tokens` that needs to be paid on top for each loan. Needs to be the same token.\\n /// @param data Additional data that was passed to the flashloan function.\\n function onBatchFlashLoan(\\n address sender,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n uint256[] calldata fees,\\n bytes calldata data\\n ) external;\\n}\\n\\n// File contracts/interfaces/IWETH.sol\\n// License-Identifier: MIT\\n\\ninterface IWETH {\\n function deposit() external payable;\\n\\n function withdraw(uint256) external;\\n}\\n\\n// File contracts/interfaces/IStrategy.sol\\n// License-Identifier: MIT\\n\\ninterface IStrategy {\\n /// @notice Send the assets to the Strategy and call skim to invest them.\\n /// @param amount The amount of tokens to invest.\\n function skim(uint256 amount) external;\\n\\n /// @notice Harvest any profits made converted to the asset and pass them to the caller.\\n /// @param balance The amount of tokens the caller thinks it has invested.\\n /// @param sender The address of the initiator of this transaction. Can be used for reimbursements, etc.\\n /// @return amountAdded The delta (+profit or -loss) that occured in contrast to `balance`.\\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\\n\\n /// @notice Withdraw assets. The returned amount can differ from the requested amount due to rounding.\\n /// @dev The `actualAmount` should be very close to the amount.\\n /// The difference should NOT be used to report a loss. That's what harvest is for.\\n /// @param amount The requested amount the caller wants to withdraw.\\n /// @return actualAmount The real amount that is withdrawn.\\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\\n\\n /// @notice Withdraw all assets in the safest way possible. This shouldn't fail.\\n /// @param balance The amount of tokens the caller thinks it has invested.\\n /// @return amountAdded The delta (+profit or -loss) that occured in contrast to `balance`.\\n function exit(uint256 balance) external returns (int256 amountAdded);\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol@v1.2.0\\n// License-Identifier: MIT\\n\\nlibrary BoringERC20 {\\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\\n\\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransfer(\\n IERC20 token,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: Transfer failed\\\");\\n }\\n\\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param from Transfer tokens from.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransferFrom(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: TransferFrom failed\\\");\\n }\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol@v1.2.0\\n// License-Identifier: MIT\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol@v1.2.0\\n// License-Identifier: MIT\\n\\nstruct Rebase {\\n uint128 elastic;\\n uint128 base;\\n}\\n\\n/// @notice A rebasing library using overflow-/underflow-safe math.\\nlibrary RebaseLibrary {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n\\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\\n function toBase(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (uint256 base) {\\n if (total.elastic == 0) {\\n base = elastic;\\n } else {\\n base = elastic.mul(total.base) / total.elastic;\\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\\n base = base.add(1);\\n }\\n }\\n }\\n\\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\\n function toElastic(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (uint256 elastic) {\\n if (total.base == 0) {\\n elastic = base;\\n } else {\\n elastic = base.mul(total.elastic) / total.base;\\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\\n elastic = elastic.add(1);\\n }\\n }\\n }\\n\\n /// @notice Add `elastic` to `total` and doubles `total.base`.\\n /// @return (Rebase) The new total.\\n /// @return base in relationship to `elastic`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 base) {\\n base = toBase(total, elastic, roundUp);\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return (total, base);\\n }\\n\\n /// @notice Sub `base` from `total` and update `total.elastic`.\\n /// @return (Rebase) The new total.\\n /// @return elastic in relationship to `base`.\\n function sub(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 elastic) {\\n elastic = toElastic(total, base, roundUp);\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return (total, elastic);\\n }\\n\\n /// @notice Add `elastic` and `base` to `total`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return total;\\n }\\n\\n /// @notice Subtract `elastic` and `base` to `total`.\\n function sub(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return total;\\n }\\n\\n /// @notice Add `elastic` to `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.add(elastic.to128());\\n }\\n\\n /// @notice Subtract `elastic` from `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\\n }\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/BoringOwnable.sol@v1.2.0\\n// License-Identifier: MIT\\n\\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\\n// Edited by BoringCrypto\\n\\ncontract BoringOwnableData {\\n address public owner;\\n address public pendingOwner;\\n}\\n\\ncontract BoringOwnable is BoringOwnableData {\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /// @notice `owner` defaults to msg.sender on construction.\\n constructor() public {\\n owner = msg.sender;\\n emit OwnershipTransferred(address(0), msg.sender);\\n }\\n\\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\\n /// Can only be invoked by the current `owner`.\\n /// @param newOwner Address of the new owner.\\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\\n function transferOwnership(\\n address newOwner,\\n bool direct,\\n bool renounce\\n ) public onlyOwner {\\n if (direct) {\\n // Checks\\n require(newOwner != address(0) || renounce, \\\"Ownable: zero address\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, newOwner);\\n owner = newOwner;\\n pendingOwner = address(0);\\n } else {\\n // Effects\\n pendingOwner = newOwner;\\n }\\n }\\n\\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\\n function claimOwnership() public {\\n address _pendingOwner = pendingOwner;\\n\\n // Checks\\n require(msg.sender == _pendingOwner, \\\"Ownable: caller != pending owner\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, _pendingOwner);\\n owner = _pendingOwner;\\n pendingOwner = address(0);\\n }\\n\\n /// @notice Only allows the `owner` to execute the function.\\n modifier onlyOwner() {\\n require(msg.sender == owner, \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol@v1.2.0\\n// License-Identifier: MIT\\n\\ninterface IMasterContract {\\n /// @notice Init function that gets called from `BoringFactory.deploy`.\\n /// Also kown as the constructor for cloned contracts.\\n /// Any ETH send to `BoringFactory.deploy` ends up here.\\n /// @param data Can be abi encoded arguments or anything else.\\n function init(bytes calldata data) external payable;\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/BoringFactory.sol@v1.2.0\\n// License-Identifier: MIT\\n\\ncontract BoringFactory {\\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\\n\\n /// @notice Mapping from clone contracts to their masterContract.\\n mapping(address => address) public masterContractOf;\\n\\n /// @notice Deploys a given master Contract as a clone.\\n /// Any ETH transferred with this call is forwarded to the new clone.\\n /// Emits `LogDeploy`.\\n /// @param masterContract The address of the contract to clone.\\n /// @param data Additional abi encoded calldata that is passed to the new clone via `IMasterContract.init`.\\n /// @param useCreate2 Creates the clone by using the CREATE2 opcode, in this case `data` will be used as salt.\\n /// @return cloneAddress Address of the created clone contract.\\n function deploy(\\n address masterContract,\\n bytes calldata data,\\n bool useCreate2\\n ) public payable returns (address cloneAddress) {\\n require(masterContract != address(0), \\\"BoringFactory: No masterContract\\\");\\n bytes20 targetBytes = bytes20(masterContract); // Takes the first 20 bytes of the masterContract's address\\n\\n if (useCreate2) {\\n // each masterContract has different code already. So clones are distinguished by their data only.\\n bytes32 salt = keccak256(data);\\n\\n // Creates clone, more info here: https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/\\n assembly {\\n let clone := mload(0x40)\\n mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)\\n mstore(add(clone, 0x14), targetBytes)\\n mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)\\n cloneAddress := create2(0, clone, 0x37, salt)\\n }\\n } else {\\n assembly {\\n let clone := mload(0x40)\\n mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)\\n mstore(add(clone, 0x14), targetBytes)\\n mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)\\n cloneAddress := create(0, clone, 0x37)\\n }\\n }\\n masterContractOf[cloneAddress] = masterContract;\\n\\n IMasterContract(cloneAddress).init{value: msg.value}(data);\\n\\n emit LogDeploy(masterContract, data, cloneAddress);\\n }\\n}\\n\\n// File contracts/MasterContractManager.sol\\n// License-Identifier: UNLICENSED\\n\\ncontract MasterContractManager is BoringOwnable, BoringFactory {\\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\\n event LogRegisterProtocol(address indexed protocol);\\n\\n /// @notice masterContract to user to approval state\\n mapping(address => mapping(address => bool)) public masterContractApproved;\\n /// @notice masterContract to whitelisted state for approval without signed message\\n mapping(address => bool) public whitelistedMasterContracts;\\n /// @notice user nonces for masterContract approvals\\n mapping(address => uint256) public nonces;\\n\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(string name,uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n bytes32 private constant APPROVAL_SIGNATURE_HASH =\\n keccak256(\\\"SetMasterContractApproval(string warning,address user,address masterContract,bool approved,uint256 nonce)\\\");\\n\\n // solhint-disable-next-line var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n // solhint-disable-next-line var-name-mixedcase\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, keccak256(\\\"BentoBox V1\\\"), chainId, address(this)));\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() public view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n /// @notice Other contracts need to register with this master contract so that users can approve them for the BentoBox.\\n function registerProtocol() public {\\n masterContractOf[msg.sender] = msg.sender;\\n emit LogRegisterProtocol(msg.sender);\\n }\\n\\n /// @notice Enables or disables a contract for approval without signed message.\\n function whitelistMasterContract(address masterContract, bool approved) public onlyOwner {\\n // Checks\\n require(masterContract != address(0), \\\"MasterCMgr: Cannot approve 0\\\");\\n\\n // Effects\\n whitelistedMasterContracts[masterContract] = approved;\\n emit LogWhiteListMasterContract(masterContract, approved);\\n }\\n\\n /// @notice Approves or revokes a `masterContract` access to `user` funds.\\n /// @param user The address of the user that approves or revokes access.\\n /// @param masterContract The address who gains or loses access.\\n /// @param approved If True approves access. If False revokes access.\\n /// @param v Part of the signature. (See EIP-191)\\n /// @param r Part of the signature. (See EIP-191)\\n /// @param s Part of the signature. (See EIP-191)\\n // F4 - Check behaviour for all function arguments when wrong or extreme\\n // F4: Don't allow masterContract 0 to be approved. Unknown contracts will have a masterContract of 0.\\n // F4: User can't be 0 for signed approvals because the recoveredAddress will be 0 if ecrecover fails\\n function setMasterContractApproval(\\n address user,\\n address masterContract,\\n bool approved,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n // Checks\\n require(masterContract != address(0), \\\"MasterCMgr: masterC not set\\\"); // Important for security\\n\\n // If no signature is provided, the fallback is executed\\n if (r == 0 && s == 0 && v == 0) {\\n require(user == msg.sender, \\\"MasterCMgr: user not sender\\\");\\n require(masterContractOf[user] == address(0), \\\"MasterCMgr: user is clone\\\");\\n require(whitelistedMasterContracts[masterContract], \\\"MasterCMgr: not whitelisted\\\");\\n } else {\\n // Important for security - any address without masterContract has address(0) as masterContract\\n // So approving address(0) would approve every address, leading to full loss of funds\\n // Also, ecrecover returns address(0) on failure. So we check this:\\n require(user != address(0), \\\"MasterCMgr: User cannot be 0\\\");\\n\\n // C10 - Protect signatures against replay, use nonce and chainId (SWC-121)\\n // C10: nonce + chainId are used to prevent replays\\n // C11 - All signatures strictly EIP-712 (SWC-117 SWC-122)\\n // C11: signature is EIP-712 compliant\\n // C12 - abi.encodePacked can't contain variable length user input (SWC-133)\\n // C12: abi.encodePacked has fixed length parameters\\n bytes32 digest =\\n keccak256(\\n abi.encodePacked(\\n EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA,\\n DOMAIN_SEPARATOR(),\\n keccak256(\\n abi.encode(\\n APPROVAL_SIGNATURE_HASH,\\n approved\\n ? keccak256(\\\"Give FULL access to funds in (and approved to) BentoBox?\\\")\\n : keccak256(\\\"Revoke access to BentoBox?\\\"),\\n user,\\n masterContract,\\n approved,\\n nonces[user]++\\n )\\n )\\n )\\n );\\n address recoveredAddress = ecrecover(digest, v, r, s);\\n require(recoveredAddress == user, \\\"MasterCMgr: Invalid Signature\\\");\\n }\\n\\n // Effects\\n masterContractApproved[masterContract][user] = approved;\\n emit LogSetMasterContractApproval(masterContract, user, approved);\\n }\\n}\\n\\n// File @boringcrypto/boring-solidity/contracts/BoringBatchable.sol@v1.2.0\\n// License-Identifier: MIT\\n\\ncontract BaseBoringBatchable {\\n /// @dev Helper function to extract a useful revert message from a failed call.\\n /// If the returned data is malformed or not correctly abi encoded then this call can fail itself.\\n function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {\\n // If the _res length is less than 68, then the transaction failed silently (without a revert message)\\n if (_returnData.length < 68) return \\\"Transaction reverted silently\\\";\\n\\n assembly {\\n // Slice the sighash.\\n _returnData := add(_returnData, 0x04)\\n }\\n return abi.decode(_returnData, (string)); // All that remains is the revert string\\n }\\n\\n /// @notice Allows batched call to self (this contract).\\n /// @param calls An array of inputs for each call.\\n /// @param revertOnFail If True then reverts after a failed call and stops doing further calls.\\n /// @return successes An array indicating the success of a call, mapped one-to-one to `calls`.\\n /// @return results An array with the returned data of each function call, mapped one-to-one to `calls`.\\n // F1: External is ok here because this is the batch function, adding it to a batch makes no sense\\n // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value\\n // C3: The length of the loop is fully under user control, so can't be exploited\\n // C7: Delegatecall is only used on the same contract, so it's safe\\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results) {\\n successes = new bool[](calls.length);\\n results = new bytes[](calls.length);\\n for (uint256 i = 0; i < calls.length; i++) {\\n (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\\n require(success || !revertOnFail, _getRevertMsg(result));\\n successes[i] = success;\\n results[i] = result;\\n }\\n }\\n}\\n\\ncontract BoringBatchable is BaseBoringBatchable {\\n /// @notice Call wrapper that performs `ERC20.permit` on `token`.\\n /// Lookup `IERC20.permit`.\\n // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert)\\n // if part of a batch this could be used to grief once as the second call would not need the permit\\n function permitToken(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n token.permit(from, to, amount, deadline, v, r, s);\\n }\\n}\\n\\n// File contracts/BentoBox.sol\\n// License-Identifier: UNLICENSED\\n\\n/// @title BentoBox\\n/// @author BoringCrypto, Keno\\n/// @notice The BentoBox is a vault for tokens. The stored tokens can be flash loaned and used in strategies.\\n/// Yield from this will go to the token depositors.\\n/// Rebasing tokens ARE NOT supported and WILL cause loss of funds.\\n/// Any funds transfered directly onto the BentoBox will be lost, use the deposit function instead.\\ncontract BentoBoxV1 is MasterContractManager, BoringBatchable {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n using BoringERC20 for IERC20;\\n using RebaseLibrary for Rebase;\\n\\n // ************** //\\n // *** EVENTS *** //\\n // ************** //\\n\\n event LogDeposit(IERC20 indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogWithdraw(IERC20 indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogTransfer(IERC20 indexed token, address indexed from, address indexed to, uint256 share);\\n\\n event LogFlashLoan(address indexed borrower, IERC20 indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\\n\\n event LogStrategyTargetPercentage(IERC20 indexed token, uint256 targetPercentage);\\n event LogStrategyQueued(IERC20 indexed token, IStrategy indexed strategy);\\n event LogStrategySet(IERC20 indexed token, IStrategy indexed strategy);\\n event LogStrategyInvest(IERC20 indexed token, uint256 amount);\\n event LogStrategyDivest(IERC20 indexed token, uint256 amount);\\n event LogStrategyProfit(IERC20 indexed token, uint256 amount);\\n event LogStrategyLoss(IERC20 indexed token, uint256 amount);\\n\\n // *************** //\\n // *** STRUCTS *** //\\n // *************** //\\n\\n struct StrategyData {\\n uint64 strategyStartDate;\\n uint64 targetPercentage;\\n uint128 balance; // the balance of the strategy that BentoBox thinks is in there\\n }\\n\\n // ******************************** //\\n // *** CONSTANTS AND IMMUTABLES *** //\\n // ******************************** //\\n\\n // V2 - Can they be private?\\n // V2: Private to save gas, to verify it's correct, check the constructor arguments\\n IERC20 private immutable wethToken;\\n\\n IERC20 private constant USE_ETHEREUM = IERC20(0);\\n uint256 private constant FLASH_LOAN_FEE = 50; // 0.05%\\n uint256 private constant FLASH_LOAN_FEE_PRECISION = 1e5;\\n uint256 private constant STRATEGY_DELAY = 2 weeks;\\n uint256 private constant MAX_TARGET_PERCENTAGE = 95; // 95%\\n uint256 private constant MINIMUM_SHARE_BALANCE = 1000; // To prevent the ratio going off\\n\\n // ***************** //\\n // *** VARIABLES *** //\\n // ***************** //\\n\\n // Balance per token per address/contract in shares\\n mapping(IERC20 => mapping(address => uint256)) public balanceOf;\\n\\n // Rebase from amount to share\\n mapping(IERC20 => Rebase) public totals;\\n\\n mapping(IERC20 => IStrategy) public strategy;\\n mapping(IERC20 => IStrategy) public pendingStrategy;\\n mapping(IERC20 => StrategyData) public strategyData;\\n\\n // ******************* //\\n // *** CONSTRUCTOR *** //\\n // ******************* //\\n\\n constructor(IERC20 wethToken_) public {\\n wethToken = wethToken_;\\n }\\n\\n // ***************** //\\n // *** MODIFIERS *** //\\n // ***************** //\\n\\n /// Modifier to check if the msg.sender is allowed to use funds belonging to the 'from' address.\\n /// If 'from' is msg.sender, it's allowed.\\n /// If 'from' is the BentoBox itself, it's allowed. Any ETH, token balances (above the known balances) or BentoBox balances\\n /// can be taken by anyone.\\n /// This is to enable skimming, not just for deposits, but also for withdrawals or transfers, enabling better composability.\\n /// If 'from' is a clone of a masterContract AND the 'from' address has approved that masterContract, it's allowed.\\n modifier allowed(address from) {\\n if (from != msg.sender && from != address(this)) {\\n // From is sender or you are skimming\\n address masterContract = masterContractOf[msg.sender];\\n require(masterContract != address(0), \\\"BentoBox: no masterContract\\\");\\n require(masterContractApproved[masterContract][from], \\\"BentoBox: Transfer not approved\\\");\\n }\\n _;\\n }\\n\\n // ************************** //\\n // *** INTERNAL FUNCTIONS *** //\\n // ************************** //\\n\\n /// @dev Returns the total balance of `token` this contracts holds,\\n /// plus the total amount this contract thinks the strategy holds.\\n function _tokenBalanceOf(IERC20 token) internal view returns (uint256 amount) {\\n amount = token.balanceOf(address(this)).add(strategyData[token].balance);\\n }\\n\\n // ************************ //\\n // *** PUBLIC FUNCTIONS *** //\\n // ************************ //\\n\\n /// @dev Helper function to represent an `amount` of `token` in shares.\\n /// @param token The ERC-20 token.\\n /// @param amount The `token` amount.\\n /// @param roundUp If the result `share` should be rounded up.\\n /// @return share The token amount represented in shares.\\n function toShare(\\n IERC20 token,\\n uint256 amount,\\n bool roundUp\\n ) external view returns (uint256 share) {\\n share = totals[token].toBase(amount, roundUp);\\n }\\n\\n /// @dev Helper function represent shares back into the `token` amount.\\n /// @param token The ERC-20 token.\\n /// @param share The amount of shares.\\n /// @param roundUp If the result should be rounded up.\\n /// @return amount The share amount back into native representation.\\n function toAmount(\\n IERC20 token,\\n uint256 share,\\n bool roundUp\\n ) external view returns (uint256 amount) {\\n amount = totals[token].toElastic(share, roundUp);\\n }\\n\\n /// @notice Deposit an amount of `token` represented in either `amount` or `share`.\\n /// @param token_ The ERC-20 token to deposit.\\n /// @param from which account to pull the tokens.\\n /// @param to which account to push the tokens.\\n /// @param amount Token amount in native representation to deposit.\\n /// @param share Token amount represented in shares to deposit. Takes precedence over `amount`.\\n /// @return amountOut The amount deposited.\\n /// @return shareOut The deposited amount repesented in shares.\\n function deposit(\\n IERC20 token_,\\n address from,\\n address to,\\n uint256 amount,\\n uint256 share\\n ) public payable allowed(from) returns (uint256 amountOut, uint256 shareOut) {\\n // Checks\\n require(to != address(0), \\\"BentoBox: to not set\\\"); // To avoid a bad UI from burning funds\\n\\n // Effects\\n IERC20 token = token_ == USE_ETHEREUM ? wethToken : token_;\\n Rebase memory total = totals[token];\\n\\n // If a new token gets added, the tokenSupply call checks that this is a deployed contract. Needed for security.\\n require(total.elastic != 0 || token.totalSupply() > 0, \\\"BentoBox: No tokens\\\");\\n if (share == 0) {\\n // value of the share may be lower than the amount due to rounding, that's ok\\n share = total.toBase(amount, false);\\n // Any deposit should lead to at least the minimum share balance, otherwise it's ignored (no amount taken)\\n if (total.base.add(share.to128()) < MINIMUM_SHARE_BALANCE) {\\n return (0, 0);\\n }\\n } else {\\n // amount may be lower than the value of share due to rounding, in that case, add 1 to amount (Always round up)\\n amount = total.toElastic(share, true);\\n }\\n\\n // In case of skimming, check that only the skimmable amount is taken.\\n // For ETH, the full balance is available, so no need to check.\\n // During flashloans the _tokenBalanceOf is lower than 'reality', so skimming deposits will mostly fail during a flashloan.\\n require(\\n from != address(this) || token_ == USE_ETHEREUM || amount <= _tokenBalanceOf(token).sub(total.elastic),\\n \\\"BentoBox: Skim too much\\\"\\n );\\n\\n balanceOf[token][to] = balanceOf[token][to].add(share);\\n total.base = total.base.add(share.to128());\\n total.elastic = total.elastic.add(amount.to128());\\n totals[token] = total;\\n\\n // Interactions\\n // During the first deposit, we check that this token is 'real'\\n if (token_ == USE_ETHEREUM) {\\n // X2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113)\\n // X2: If the WETH implementation is faulty or malicious, it will block adding ETH (but we know the WETH implementation)\\n IWETH(address(wethToken)).deposit{value: amount}();\\n } else if (from != address(this)) {\\n // X2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113)\\n // X2: If the token implementation is faulty or malicious, it may block adding tokens. Good.\\n token.safeTransferFrom(from, address(this), amount);\\n }\\n emit LogDeposit(token, from, to, amount, share);\\n amountOut = amount;\\n shareOut = share;\\n }\\n\\n /// @notice Withdraws an amount of `token` from a user account.\\n /// @param token_ The ERC-20 token to withdraw.\\n /// @param from which user to pull the tokens.\\n /// @param to which user to push the tokens.\\n /// @param amount of tokens. Either one of `amount` or `share` needs to be supplied.\\n /// @param share Like above, but `share` takes precedence over `amount`.\\n function withdraw(\\n IERC20 token_,\\n address from,\\n address to,\\n uint256 amount,\\n uint256 share\\n ) public allowed(from) returns (uint256 amountOut, uint256 shareOut) {\\n // Checks\\n require(to != address(0), \\\"BentoBox: to not set\\\"); // To avoid a bad UI from burning funds\\n\\n // Effects\\n IERC20 token = token_ == USE_ETHEREUM ? wethToken : token_;\\n Rebase memory total = totals[token];\\n if (share == 0) {\\n // value of the share paid could be lower than the amount paid due to rounding, in that case, add a share (Always round up)\\n share = total.toBase(amount, true);\\n } else {\\n // amount may be lower than the value of share due to rounding, that's ok\\n amount = total.toElastic(share, false);\\n }\\n\\n balanceOf[token][from] = balanceOf[token][from].sub(share);\\n total.elastic = total.elastic.sub(amount.to128());\\n total.base = total.base.sub(share.to128());\\n // There have to be at least 1000 shares left to prevent reseting the share/amount ratio (unless it's fully emptied)\\n require(total.base >= MINIMUM_SHARE_BALANCE || total.base == 0, \\\"BentoBox: cannot empty\\\");\\n totals[token] = total;\\n\\n // Interactions\\n if (token_ == USE_ETHEREUM) {\\n // X2, X3: A revert or big gas usage in the WETH contract could block withdrawals, but WETH9 is fine.\\n IWETH(address(wethToken)).withdraw(amount);\\n // X2, X3: A revert or big gas usage could block, however, the to address is under control of the caller.\\n (bool success, ) = to.call{value: amount}(\\\"\\\");\\n require(success, \\\"BentoBox: ETH transfer failed\\\");\\n } else {\\n // X2, X3: A malicious token could block withdrawal of just THAT token.\\n // masterContracts may want to take care not to rely on withdraw always succeeding.\\n token.safeTransfer(to, amount);\\n }\\n emit LogWithdraw(token, from, to, amount, share);\\n amountOut = amount;\\n shareOut = share;\\n }\\n\\n /// @notice Transfer shares from a user account to another one.\\n /// @param token The ERC-20 token to transfer.\\n /// @param from which user to pull the tokens.\\n /// @param to which user to push the tokens.\\n /// @param share The amount of `token` in shares.\\n // Clones of master contracts can transfer from any account that has approved them\\n // F3 - Can it be combined with another similar function?\\n // F3: This isn't combined with transferMultiple for gas optimization\\n function transfer(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 share\\n ) public allowed(from) {\\n // Checks\\n require(to != address(0), \\\"BentoBox: to not set\\\"); // To avoid a bad UI from burning funds\\n\\n // Effects\\n balanceOf[token][from] = balanceOf[token][from].sub(share);\\n balanceOf[token][to] = balanceOf[token][to].add(share);\\n\\n emit LogTransfer(token, from, to, share);\\n }\\n\\n /// @notice Transfer shares from a user account to multiple other ones.\\n /// @param token The ERC-20 token to transfer.\\n /// @param from which user to pull the tokens.\\n /// @param tos The receivers of the tokens.\\n /// @param shares The amount of `token` in shares for each receiver in `tos`.\\n // F3 - Can it be combined with another similar function?\\n // F3: This isn't combined with transfer for gas optimization\\n function transferMultiple(\\n IERC20 token,\\n address from,\\n address[] calldata tos,\\n uint256[] calldata shares\\n ) public allowed(from) {\\n // Checks\\n require(tos[0] != address(0), \\\"BentoBox: to[0] not set\\\"); // To avoid a bad UI from burning funds\\n\\n // Effects\\n uint256 totalAmount;\\n uint256 len = tos.length;\\n for (uint256 i = 0; i < len; i++) {\\n address to = tos[i];\\n balanceOf[token][to] = balanceOf[token][to].add(shares[i]);\\n totalAmount = totalAmount.add(shares[i]);\\n emit LogTransfer(token, from, to, shares[i]);\\n }\\n balanceOf[token][from] = balanceOf[token][from].sub(totalAmount);\\n }\\n\\n /// @notice Flashloan ability.\\n /// @param borrower The address of the contract that implements and conforms to `IFlashBorrower` and handles the flashloan.\\n /// @param receiver Address of the token receiver.\\n /// @param token The address of the token to receive.\\n /// @param amount of the tokens to receive.\\n /// @param data The calldata to pass to the `borrower` contract.\\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\\n // F5: Not possible to follow this here, reentrancy has been reviewed\\n // F6 - Check for front-running possibilities, such as the approve function (SWC-114)\\n // F6: Slight grieving possible by withdrawing an amount before someone tries to flashloan close to the full amount.\\n function flashLoan(\\n IFlashBorrower borrower,\\n address receiver,\\n IERC20 token,\\n uint256 amount,\\n bytes calldata data\\n ) public {\\n uint256 fee = amount.mul(FLASH_LOAN_FEE) / FLASH_LOAN_FEE_PRECISION;\\n token.safeTransfer(receiver, amount);\\n\\n borrower.onFlashLoan(msg.sender, token, amount, fee, data);\\n\\n require(_tokenBalanceOf(token) >= totals[token].addElastic(fee.to128()), \\\"BentoBox: Wrong amount\\\");\\n emit LogFlashLoan(address(borrower), token, amount, fee, receiver);\\n }\\n\\n /// @notice Support for batched flashloans. Useful to request multiple different `tokens` in a single transaction.\\n /// @param borrower The address of the contract that implements and conforms to `IBatchFlashBorrower` and handles the flashloan.\\n /// @param receivers An array of the token receivers. A one-to-one mapping with `tokens` and `amounts`.\\n /// @param tokens The addresses of the tokens.\\n /// @param amounts of the tokens for each receiver.\\n /// @param data The calldata to pass to the `borrower` contract.\\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\\n // F5: Not possible to follow this here, reentrancy has been reviewed\\n // F6 - Check for front-running possibilities, such as the approve function (SWC-114)\\n // F6: Slight grieving possible by withdrawing an amount before someone tries to flashloan close to the full amount.\\n function batchFlashLoan(\\n IBatchFlashBorrower borrower,\\n address[] calldata receivers,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n bytes calldata data\\n ) public {\\n uint256[] memory fees = new uint256[](tokens.length);\\n\\n uint256 len = tokens.length;\\n for (uint256 i = 0; i < len; i++) {\\n uint256 amount = amounts[i];\\n fees[i] = amount.mul(FLASH_LOAN_FEE) / FLASH_LOAN_FEE_PRECISION;\\n\\n tokens[i].safeTransfer(receivers[i], amounts[i]);\\n }\\n\\n borrower.onBatchFlashLoan(msg.sender, tokens, amounts, fees, data);\\n\\n for (uint256 i = 0; i < len; i++) {\\n IERC20 token = tokens[i];\\n require(_tokenBalanceOf(token) >= totals[token].addElastic(fees[i].to128()), \\\"BentoBox: Wrong amount\\\");\\n emit LogFlashLoan(address(borrower), token, amounts[i], fees[i], receivers[i]);\\n }\\n }\\n\\n /// @notice Sets the target percentage of the strategy for `token`.\\n /// @dev Only the owner of this contract is allowed to change this.\\n /// @param token The address of the token that maps to a strategy to change.\\n /// @param targetPercentage_ The new target in percent. Must be lesser or equal to `MAX_TARGET_PERCENTAGE`.\\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) public onlyOwner {\\n // Checks\\n require(targetPercentage_ <= MAX_TARGET_PERCENTAGE, \\\"StrategyManager: Target too high\\\");\\n\\n // Effects\\n strategyData[token].targetPercentage = targetPercentage_;\\n emit LogStrategyTargetPercentage(token, targetPercentage_);\\n }\\n\\n /// @notice Sets the contract address of a new strategy that conforms to `IStrategy` for `token`.\\n /// Must be called twice with the same arguments.\\n /// A new strategy becomes pending first and can be activated once `STRATEGY_DELAY` is over.\\n /// @dev Only the owner of this contract is allowed to change this.\\n /// @param token The address of the token that maps to a strategy to change.\\n /// @param newStrategy The address of the contract that conforms to `IStrategy`.\\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\\n // F5: Total amount is updated AFTER interaction. But strategy is under our control.\\n // C4 - Use block.timestamp only for long intervals (SWC-116)\\n // C4: block.timestamp is used for a period of 2 weeks, which is long enough\\n function setStrategy(IERC20 token, IStrategy newStrategy) public onlyOwner {\\n StrategyData memory data = strategyData[token];\\n IStrategy pending = pendingStrategy[token];\\n if (data.strategyStartDate == 0 || pending != newStrategy) {\\n pendingStrategy[token] = newStrategy;\\n // C1 - All math done through BoringMath (SWC-101)\\n // C1: Our sun will swallow the earth well before this overflows\\n data.strategyStartDate = (block.timestamp + STRATEGY_DELAY).to64();\\n emit LogStrategyQueued(token, newStrategy);\\n } else {\\n require(data.strategyStartDate != 0 && block.timestamp >= data.strategyStartDate, \\\"StrategyManager: Too early\\\");\\n if (address(strategy[token]) != address(0)) {\\n int256 balanceChange = strategy[token].exit(data.balance);\\n // Effects\\n if (balanceChange > 0) {\\n uint256 add = uint256(balanceChange);\\n totals[token].addElastic(add);\\n emit LogStrategyProfit(token, add);\\n } else if (balanceChange < 0) {\\n uint256 sub = uint256(-balanceChange);\\n totals[token].subElastic(sub);\\n emit LogStrategyLoss(token, sub);\\n }\\n\\n emit LogStrategyDivest(token, data.balance);\\n }\\n strategy[token] = pending;\\n data.strategyStartDate = 0;\\n data.balance = 0;\\n pendingStrategy[token] = IStrategy(0);\\n emit LogStrategySet(token, newStrategy);\\n }\\n strategyData[token] = data;\\n }\\n\\n /// @notice The actual process of yield farming. Executes the strategy of `token`.\\n /// Optionally does housekeeping if `balance` is true.\\n /// `maxChangeAmount` is relevant for skimming or withdrawing if `balance` is true.\\n /// @param token The address of the token for which a strategy is deployed.\\n /// @param balance True if housekeeping should be done.\\n /// @param maxChangeAmount The maximum amount for either pulling or pushing from/to the `IStrategy` contract.\\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\\n // F5: Total amount is updated AFTER interaction. But strategy is under our control.\\n // F5: Not followed to prevent reentrancy issues with flashloans and BentoBox skims?\\n function harvest(\\n IERC20 token,\\n bool balance,\\n uint256 maxChangeAmount\\n ) public {\\n StrategyData memory data = strategyData[token];\\n IStrategy _strategy = strategy[token];\\n int256 balanceChange = _strategy.harvest(data.balance, msg.sender);\\n if (balanceChange == 0 && !balance) {\\n return;\\n }\\n\\n uint256 totalElastic = totals[token].elastic;\\n\\n if (balanceChange > 0) {\\n uint256 add = uint256(balanceChange);\\n totalElastic = totalElastic.add(add);\\n totals[token].elastic = totalElastic.to128();\\n emit LogStrategyProfit(token, add);\\n } else if (balanceChange < 0) {\\n // C1 - All math done through BoringMath (SWC-101)\\n // C1: balanceChange could overflow if it's max negative int128.\\n // But tokens with balances that large are not supported by the BentoBox.\\n uint256 sub = uint256(-balanceChange);\\n totalElastic = totalElastic.sub(sub);\\n totals[token].elastic = totalElastic.to128();\\n data.balance = data.balance.sub(sub.to128());\\n emit LogStrategyLoss(token, sub);\\n }\\n\\n if (balance) {\\n uint256 targetBalance = totalElastic.mul(data.targetPercentage) / 100;\\n // if data.balance == targetBalance there is nothing to update\\n if (data.balance < targetBalance) {\\n uint256 amountOut = targetBalance.sub(data.balance);\\n if (maxChangeAmount != 0 && amountOut > maxChangeAmount) {\\n amountOut = maxChangeAmount;\\n }\\n token.safeTransfer(address(_strategy), amountOut);\\n data.balance = data.balance.add(amountOut.to128());\\n _strategy.skim(amountOut);\\n emit LogStrategyInvest(token, amountOut);\\n } else if (data.balance > targetBalance) {\\n uint256 amountIn = data.balance.sub(targetBalance.to128());\\n if (maxChangeAmount != 0 && amountIn > maxChangeAmount) {\\n amountIn = maxChangeAmount;\\n }\\n\\n uint256 actualAmountIn = _strategy.withdraw(amountIn);\\n\\n data.balance = data.balance.sub(actualAmountIn.to128());\\n emit LogStrategyDivest(token, actualAmountIn);\\n }\\n }\\n\\n strategyData[token] = data;\\n }\\n\\n // Contract should be able to receive ETH deposits to support deposit & skim\\n // solhint-disable-next-line no-empty-blocks\\n receive() external payable {}\\n}\\n\",\"keccak256\":\"0xf5251397b9aaa5c66ddc18b0cb96350ffac09f5776a1255fedc0072641f916fe\",\"license\":\"UNLICENSED\"},\"contracts/mocks/BentoBoxMock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\nimport \\\"../BentoBoxFlat.sol\\\";\\n\\ncontract BentoBoxMock is BentoBoxV1 {\\n constructor(IERC20 weth) public BentoBoxV1(weth) {\\n return;\\n }\\n\\n function addProfit(IERC20 token, uint256 amount) public {\\n token.safeTransferFrom(msg.sender, address(this), amount);\\n totals[token].addElastic(amount);\\n }\\n\\n function takeLoss(IERC20 token, uint256 amount) public {\\n token.safeTransfer(msg.sender, amount);\\n totals[token].subElastic(amount);\\n }\\n}\\n\",\"keccak256\":\"0x064d7054b250451076169b917a3ba281b7e3fe4a2bf265d8687460716019fb8c\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60e06040523480156200001157600080fd5b50604051620048c4380380620048c4833981016040819052620000349162000117565b600080546001600160a01b0319163390811782556040518392907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a34660a081905262000084816200009f565b6080525060601b6001600160601b03191660c052506200016b565b60007f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a8667fd7df266aff736d415a9dc14b4158201d612e70d75b9c7f4e375ccfd20aa5166f8330604051602001620000fa949392919062000147565b604051602081830303815290604052805190602001209050919050565b60006020828403121562000129578081fd5b81516001600160a01b038116811462000140578182fd5b9392505050565b938452602084019290925260408301526001600160a01b0316606082015260800190565b60805160a05160c05160601c61471a620001aa6000398061071b52806109e85280611f5d528061213e5250806110ae5250806110e3525061471a6000f3fe6080604052600436106101f25760003560e01c80637c516e941161010d578063c0a47c93116100a0578063e30c39781161006f578063e30c397814610596578063f1676d37146105ab578063f18d03cc146105cb578063f483b3da146105eb578063f7888aec1461060b576101f9565b8063c0a47c9314610506578063d2423b5114610526578063da5139ca14610547578063df23b45b14610567576101f9565b806397da6d30116100dc57806397da6d30146104915780639a7a4b5d146104b1578063aee4d1b2146104d1578063bafe4f14146104e6576101f9565b80637c516e941461041c5780637ecebe001461043c5780638da5cb5b1461045c57806391e0eab514610471576101f9565b80633e2a9d4e116101855780635662311811610154578063566231181461039c57806366c6bb0b146103bc57806372cb5d97146103dc578063733a9d7c146103fc576101f9565b80633e2a9d4e146103195780634e71e0c8146103395780634ffe34db1461034e5780635108a5581461037c576101f9565b80631583d56c116101c15780631583d56c146102975780631f54245b146102b7578063228bfd9f146102d75780633644e515146102f7576101f9565b806302b9446c146101fe578063078dfbe7146102285780630fca88431461024a57806312a90c8a1461026a576101f9565b366101f957005b600080fd5b61021161020c366004613858565b61062b565b60405161021f9291906145f0565b60405180910390f35b34801561023457600080fd5b5061024861024336600461360f565b610aef565b005b34801561025657600080fd5b50610248610265366004613933565b610bd5565b34801561027657600080fd5b5061028a610285366004613522565b610e86565b60405161021f9190613eef565b3480156102a357600080fd5b506102486102b2366004613a04565b610e9b565b6102ca6102c5366004613659565b610ed2565b60405161021f9190613cb6565b3480156102e357600080fd5b506102ca6102f2366004613522565b61108e565b34801561030357600080fd5b5061030c6110a9565b60405161021f9190613efa565b34801561032557600080fd5b50610248610334366004613a65565b611109565b34801561034557600080fd5b506102486111d4565b34801561035a57600080fd5b5061036e610369366004613522565b611261565b60405161021f9291906145d6565b34801561038857600080fd5b506102ca610397366004613522565b611287565b3480156103a857600080fd5b5061030c6103b7366004613a2f565b6112a2565b3480156103c857600080fd5b506102486103d73660046139c4565b6112f6565b3480156103e857600080fd5b506102486103f73660046137f6565b6118bf565b34801561040857600080fd5b506102486104173660046135e2565b611d1d565b34801561042857600080fd5b506102486104373660046138b2565b611dc1565b34801561044857600080fd5b5061030c610457366004613522565b611e35565b34801561046857600080fd5b506102ca611e47565b34801561047d57600080fd5b5061028a61048c36600461353e565b611e56565b34801561049d57600080fd5b506102116104ac366004613858565b611e76565b3480156104bd57600080fd5b506102486104cc366004613a04565b61228a565b3480156104dd57600080fd5b506102486122c0565b3480156104f257600080fd5b506102ca610501366004613522565b612307565b34801561051257600080fd5b50610248610521366004613576565b612322565b6105396105343660046136be565b612631565b60405161021f929190613e55565b34801561055357600080fd5b5061030c610562366004613a2f565b6127c1565b34801561057357600080fd5b50610587610582366004613522565b61280d565b60405161021f93929190614612565b3480156105a257600080fd5b506102ca612846565b3480156105b757600080fd5b506102486105c6366004613a9d565b612855565b3480156105d757600080fd5b506102486105e6366004613808565b6129a6565b3480156105f757600080fd5b50610248610606366004613723565b612b4a565b34801561061757600080fd5b5061030c6106263660046137f6565b612e0a565b600080856001600160a01b038116331480159061065157506001600160a01b0381163014155b156106dc57336000908152600260205260409020546001600160a01b0316806106955760405162461bcd60e51b815260040161068c906143f3565b60405180910390fd5b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff166106da5760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b0386166107025760405162461bcd60e51b815260040161068c906141dc565b60006001600160a01b03891615610719578861073b565b7f00000000000000000000000000000000000000000000000000000000000000005b9050610745613448565b506001600160a01b0381166000908152600760209081526040918290208251808401909352546001600160801b03808216808552600160801b909204169183019190915215158061080657506000826001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156107cc57600080fd5b505afa1580156107e0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108049190613b0d565b115b6108225760405162461bcd60e51b815260040161068c9061410a565b8561087a5761083381886000612e27565b95506103e861085861084488612ec1565b60208401516001600160801b031690612eee565b6001600160801b0316101561087557600080945094505050610ae4565b610889565b61088681876001612f23565b96505b6001600160a01b038916301415806108a857506001600160a01b038a16155b806108d0575080516108cc906001600160801b03166108c684612fa2565b9061304a565b8711155b6108ec5760405162461bcd60e51b815260040161068c90613fcf565b6001600160a01b038083166000908152600660209081526040808320938c168352929052205461091c908761306d565b6001600160a01b038084166000908152600660209081526040808320938d168352929052205561096261094e87612ec1565b60208301516001600160801b031690612eee565b6001600160801b0316602082015261098d61097c88612ec1565b82516001600160801b031690612eee565b6001600160801b0390811682526001600160a01b03808416600090815260076020908152604090912084518154928601518516600160801b029085166001600160801b031990931692909217909316179091558a16610a5f577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663d0e30db0886040518263ffffffff1660e01b81526004016000604051808303818588803b158015610a4157600080fd5b505af1158015610a55573d6000803e3d6000fd5b5050505050610a84565b6001600160a01b0389163014610a8457610a846001600160a01b0383168a308a613090565b876001600160a01b0316896001600160a01b0316836001600160a01b03167fb2346165e782564f17f5b7e555c21f4fd96fbc93458572bf0113ea35a958fc558a8a604051610ad39291906145f0565b60405180910390a486945085935050505b509550959350505050565b6000546001600160a01b03163314610b195760405162461bcd60e51b815260040161068c90614241565b8115610bb4576001600160a01b038316151580610b335750805b610b4f5760405162461bcd60e51b815260040161068c906140db565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b031991821617909155600180549091169055610bd0565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b846001600160a01b0381163314801590610bf857506001600160a01b0381163014155b15610c7a57336000908152600260205260409020546001600160a01b031680610c335760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16610c785760405162461bcd60e51b815260040161068c90614276565b505b600085858281610c8657fe5b9050602002016020810190610c9b9190613522565b6001600160a01b03161415610cc25760405162461bcd60e51b815260040161068c906141a5565b600084815b81811015610e1e576000888883818110610cdd57fe5b9050602002016020810190610cf29190613522565b9050610d61878784818110610d0357fe5b90506020020135600660008e6001600160a01b03166001600160a01b031681526020019081526020016000206000846001600160a01b03166001600160a01b031681526020019081526020016000205461306d90919063ffffffff16565b6001600160a01b03808d16600090815260066020908152604080832093861683529290522055610dac878784818110610d9657fe5b905060200201358561306d90919063ffffffff16565b9350806001600160a01b03168a6001600160a01b03168c6001600160a01b03167f6eabe333476233fd382224f233210cb808a7bc4c4de64f9d76628bf63c677b1a8a8a87818110610df957fe5b90506020020135604051610e0d9190613efa565b60405180910390a450600101610cc7565b506001600160a01b03808a166000908152600660209081526040808320938c1683529290522054610e4f908361304a565b6001600160a01b03998a1660009081526006602090815260408083209b909c16825299909952989097209790975550505050505050565b60046020526000908152604090205460ff1681565b610eb06001600160a01b038316333084613090565b6001600160a01b0382166000908152600760205260409020610bd09082613189565b60006001600160a01b038516610efa5760405162461bcd60e51b815260040161068c90614387565b606085901b8215610f6c5760008585604051610f17929190613c60565b60405180910390209050604051733d602d80600a3d3981f3363d3d373d3d3d363d7360601b81528260148201526e5af43d82803e903d91602b57fd5bf360881b6028820152816037826000f593505050610fb1565b604051733d602d80600a3d3981f3363d3d373d3d3d363d7360601b81528160148201526e5af43d82803e903d91602b57fd5bf360881b60288201526037816000f09250505b6001600160a01b038281166000818152600260205260409081902080546001600160a01b031916938a16939093179092559051631377d1f560e21b8152634ddf47d49034906110069089908990600401613f79565b6000604051808303818588803b15801561101f57600080fd5b505af1158015611033573d6000803e3d6000fd5b5050505050816001600160a01b0316866001600160a01b03167fd62166f3c2149208e51788b1401cc356bf5da1fc6c7886a32e18570f57d88b3b878760405161107d929190613f79565b60405180910390a350949350505050565b6008602052600090815260409020546001600160a01b031681565b6000467f000000000000000000000000000000000000000000000000000000000000000081146110e1576110dc816131cd565b611103565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b6000546001600160a01b031633146111335760405162461bcd60e51b815260040161068c90614241565b605f816001600160401b0316111561115d5760405162461bcd60e51b815260040161068c9061456b565b6001600160a01b0382166000818152600a602052604090819020805467ffffffffffffffff60401b1916600160401b6001600160401b03861602179055517f7543af99b5602c06e62da0631b5308489a5ff859150105a623b6eb15e8deae0b906111c89084906145fe565b60405180910390a25050565b6001546001600160a01b03163381146111ff5760405162461bcd60e51b815260040161068c906142ad565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b6007602052600090815260409020546001600160801b0380821691600160801b90041682565b6009602052600090815260409020546001600160a01b031681565b6001600160a01b03831660009081526007602090815260408083208151808301909252546001600160801b038082168352600160801b90910416918101919091526112ee908484612f23565b949350505050565b6112fe61345f565b506001600160a01b038381166000818152600a60209081526040808320815160608101835290546001600160401b038082168352600160401b82041682850152600160801b90046001600160801b031681830190815294845260089092528083205493519051630c7e663b60e11b81529194939093169283916318fccc769161138b9133906004016145b4565b602060405180830381600087803b1580156113a557600080fd5b505af11580156113b9573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113dd9190613b0d565b9050801580156113eb575084155b156113f857505050610bd0565b6001600160a01b0386166000908152600760205260408120546001600160801b0316908213156114ae578161142d828261306d565b915061143882612ec1565b6001600160a01b0389166000818152600760205260409081902080546001600160801b0319166001600160801b03949094169390931790925590517f911c9f20a03edabcbcbd18dca1174cce47a91b234ced7a5a3c60ba0d5b56c5d2906114a0908490613efa565b60405180910390a25061157c565b600082121561157c5760008290036114c6828261304a565b91506114d182612ec1565b6001600160a01b038916600090815260076020526040902080546001600160801b0319166001600160801b039290921691909117905561152761151382612ec1565b60408701516001600160801b031690613244565b6001600160801b0316604080870191909152516001600160a01b038916907f8f1f26eb9b6aa8689dbdd519ead1999d9c8819d4738e403b2003b18197d9cf9790611572908490613efa565b60405180910390a2505b851561183b57600060646115a686602001516001600160401b03168461327390919063ffffffff16565b816115ad57fe5b0490508085604001516001600160801b031610156116e85760006115e786604001516001600160801b03168361304a90919063ffffffff16565b905086158015906115f757508681115b156115ff5750855b6116136001600160a01b038a1686836132aa565b61163361161f82612ec1565b60408801516001600160801b031690612eee565b6001600160801b031660408088019190915251636939aaf560e01b81526001600160a01b03861690636939aaf59061166f908490600401613efa565b600060405180830381600087803b15801561168957600080fd5b505af115801561169d573d6000803e3d6000fd5b50505050886001600160a01b03167fb18e7e4f6eac147a63a3bb6beb2d9039c88698623aff3efc4febbc20b0164ee5826040516116da9190613efa565b60405180910390a250611839565b8085604001516001600160801b0316111561183957600061171f61170b83612ec1565b60408801516001600160801b031690613244565b6001600160801b03169050861580159061173857508681115b156117405750855b604051632e1a7d4d60e01b81526000906001600160a01b03871690632e1a7d4d9061176f908590600401613efa565b602060405180830381600087803b15801561178957600080fd5b505af115801561179d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117c19190613b0d565b90506117e36117cf82612ec1565b60408901516001600160801b031690613244565b6001600160801b0316604080890191909152516001600160a01b038b16907f39aa22060f8dd4d291720311feedf3b72fef47c06c66ccf5c22b502c62e7550a9061182e908490613efa565b60405180910390a250505b505b5050506001600160a01b0384166000908152600a6020908152604091829020835181549285015193909401516001600160801b03908116600160801b026001600160401b03948516600160401b0267ffffffffffffffff60401b199590961667ffffffffffffffff1990941693909317939093169390931791909116179055505050565b6000546001600160a01b031633146118e95760405162461bcd60e51b815260040161068c90614241565b6118f161345f565b506001600160a01b038281166000818152600a60209081526040808320815160608101835290546001600160401b038082168352600160401b8204811683860152600160801b9091046001600160801b0316828401529484526009909252909120548151919316911615806119785750826001600160a01b0316816001600160a01b031614155b15611a02576001600160a01b03848116600090815260096020526040902080546001600160a01b0319169185169190911790556119b96212750042016133a0565b6001600160401b031682526040516001600160a01b0380851691908616907f6f7ccdf3f86039e5a1dcf6028bf7b4773cbf7a234716ba2e5392b12bb0f8558f90600090a3611c9d565b81516001600160401b031615801590611a25575081516001600160401b03164210155b611a415760405162461bcd60e51b815260040161068c9061420a565b6001600160a01b038481166000908152600860205260409020541615611c26576001600160a01b0380851660009081526008602052604080822054858201519151637f8661a160e01b815292931691637f8661a191611aa2916004016145a0565b602060405180830381600087803b158015611abc57600080fd5b505af1158015611ad0573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611af49190613b0d565b90506000811315611b6b576001600160a01b03851660009081526007602052604090208190611b239082613189565b50856001600160a01b03167f911c9f20a03edabcbcbd18dca1174cce47a91b234ced7a5a3c60ba0d5b56c5d282604051611b5d9190613efa565b60405180910390a250611bdf565b6000811215611bdf576001600160a01b03851660009081526007602052604081209082900390611b9b90826133c9565b50856001600160a01b03167f8f1f26eb9b6aa8689dbdd519ead1999d9c8819d4738e403b2003b18197d9cf9782604051611bd59190613efa565b60405180910390a2505b846001600160a01b03167f39aa22060f8dd4d291720311feedf3b72fef47c06c66ccf5c22b502c62e7550a8460400151604051611c1c91906145a0565b60405180910390a2505b6001600160a01b03808516600081815260086020908152604080832080548688166001600160a01b0319918216179091558388528782018490528484526009909252808320805490921690915551928616927f03e6352a885adc4cc54767592939c3b1bbd65685658c3beaaba66a888120e2179190a35b506001600160a01b03929092166000908152600a60209081526040918290208451815492860151939095015167ffffffffffffffff199092166001600160401b039586161767ffffffffffffffff60401b1916600160401b9590931694909402919091176001600160801b03908116600160801b91909216021790915550565b6000546001600160a01b03163314611d475760405162461bcd60e51b815260040161068c90614241565b6001600160a01b038216611d6d5760405162461bcd60e51b815260040161068c90614006565b6001600160a01b03821660008181526004602052604090819020805460ff1916841515179055517f31a1e0eac44b54ac6c2a2efa87e92c83405ffcf33fceef02a7bca695130e2600906111c8908490613eef565b60405163d505accf60e01b81526001600160a01b0389169063d505accf90611df9908a908a908a908a908a908a908a90600401613dfb565b600060405180830381600087803b158015611e1357600080fd5b505af1158015611e27573d6000803e3d6000fd5b505050505050505050505050565b60056020526000908152604090205481565b6000546001600160a01b031681565b600360209081526000928352604080842090915290825290205460ff1681565b600080856001600160a01b0381163314801590611e9c57506001600160a01b0381163014155b15611f1e57336000908152600260205260409020546001600160a01b031680611ed75760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16611f1c5760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b038616611f445760405162461bcd60e51b815260040161068c906141dc565b60006001600160a01b03891615611f5b5788611f7d565b7f00000000000000000000000000000000000000000000000000000000000000005b9050611f87613448565b506001600160a01b0381166000908152600760209081526040918290208251808401909352546001600160801b038082168452600160801b909104169082015285611fdf57611fd881886001612e27565b9550611fee565b611feb81876000612f23565b96505b6001600160a01b038083166000908152600660209081526040808320938d168352929052205461201e908761304a565b6001600160a01b038084166000908152600660209081526040808320938e168352929052205561206161205088612ec1565b82516001600160801b031690613244565b6001600160801b0316815261208c61207887612ec1565b60208301516001600160801b031690613244565b6001600160801b0316602082018190526103e81115806120b7575060208101516001600160801b0316155b6120d35760405162461bcd60e51b815260040161068c906140ab565b6001600160a01b03828116600090815260076020908152604090912083518154928501516001600160801b03199093166001600160801b03918216178116600160801b91909316029190911790558a1661222757604051632e1a7d4d60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690632e1a7d4d90612173908a90600401613efa565b600060405180830381600087803b15801561218d57600080fd5b505af11580156121a1573d6000803e3d6000fd5b505050506000886001600160a01b0316886040516121be90613cb3565b60006040518083038185875af1925050503d80600081146121fb576040519150601f19603f3d011682016040523d82523d6000602084013e612200565b606091505b50509050806122215760405162461bcd60e51b815260040161068c906144fd565b5061223b565b61223b6001600160a01b03831689896132aa565b876001600160a01b0316896001600160a01b0316836001600160a01b03167fad9ab9ee6953d4d177f4a03b3a3ac3178ffcb9816319f348060194aa76b144868a8a604051610ad39291906145f0565b61229e6001600160a01b03831633836132aa565b6001600160a01b0382166000908152600760205260409020610bd090826133c9565b3360008181526002602052604080822080546001600160a01b03191684179055517fdfb44ffabf0d3a8f650d3ce43eff98f6d050e7ea1a396d5794f014e7dadabacb9190a2565b6002602052600090815260409020546001600160a01b031681565b6001600160a01b0385166123485760405162461bcd60e51b815260040161068c906143bc565b81158015612354575080155b8015612361575060ff8316155b15612403576001600160a01b038616331461238e5760405162461bcd60e51b815260040161068c9061403d565b6001600160a01b0386811660009081526002602052604090205416156123c65760405162461bcd60e51b815260040161068c90614319565b6001600160a01b03851660009081526004602052604090205460ff166123fe5760405162461bcd60e51b815260040161068c906144c6565b6125bd565b6001600160a01b0386166124295760405162461bcd60e51b815260040161068c9061445f565b600060405180604001604052806002815260200161190160f01b81525061244e6110a9565b7f1962bc9f5484cb7a998701b81090e966ee1fce5771af884cceee7c081b14ade28761249a577fb426802f1f7dc850a7b6b38805edea2442f3992878a9ab985abfe8091d95d0b16124bc565b7f422ac5323fe049241dee67716229a1cc1bc7b313b23dfe3ef6d42ab177a3b2845b6001600160a01b038b1660009081526005602090815260409182902080546001810190915591516124f69493928e928e928e929101613f03565b6040516020818303038152906040528051906020012060405160200161251e93929190613c8c565b60405160208183030381529060405280519060200120905060006001828686866040516000815260200160405260405161255b9493929190613f5b565b6020604051602081039080840390855afa15801561257d573d6000803e3d6000fd5b505050602060405103519050876001600160a01b0316816001600160a01b0316146125ba5760405162461bcd60e51b815260040161068c90614350565b50505b6001600160a01b038581166000818152600360209081526040808320948b168084529490915290819020805460ff1916881515179055517f5f6ebb64ba012a851c6f014e6cad458ddf213d1512049b31cd06365c2b05925790612621908890613eef565b60405180910390a3505050505050565b606080836001600160401b038111801561264a57600080fd5b50604051908082528060200260200182016040528015612674578160200160208202803683370190505b509150836001600160401b038111801561268d57600080fd5b506040519080825280602002602001820160405280156126c157816020015b60608152602001906001900390816126ac5790505b50905060005b848110156127b85760006060308888858181106126e057fe5b90506020028101906126f2919061463e565b604051612700929190613c60565b600060405180830381855af49150503d806000811461273b576040519150601f19603f3d011682016040523d82523d6000602084013e612740565b606091505b5091509150818061274f575085155b612758826133e8565b906127765760405162461bcd60e51b815260040161068c9190613f8d565b508185848151811061278457fe5b602002602001019015159081151581525050808484815181106127a357fe5b602090810291909101015250506001016126c7565b50935093915050565b6001600160a01b03831660009081526007602090815260408083208151808301909252546001600160801b038082168352600160801b90910416918101919091526112ee908484612e27565b600a602052600090815260409020546001600160401b0380821691600160401b810490911690600160801b90046001600160801b031683565b6001546001600160a01b031681565b6000620186a0612866856032613273565b8161286d57fe5b0490506128846001600160a01b03861687866132aa565b6040516323e30c8b60e01b81526001600160a01b038816906323e30c8b906128ba9033908990899087908a908a90600401613d90565b600060405180830381600087803b1580156128d457600080fd5b505af11580156128e8573d6000803e3d6000fd5b5050505061291f6128f882612ec1565b6001600160a01b0387166000908152600760205260409020906001600160801b0316613189565b61292886612fa2565b10156129465760405162461bcd60e51b815260040161068c90614496565b856001600160a01b0316856001600160a01b0316886001600160a01b03167f3be9b85936d5d30a1655ea116a17ee3d827b2cd428cc026ce5bf2ac46a22320487856040516129959291906145f0565b60405180910390a450505050505050565b826001600160a01b03811633148015906129c957506001600160a01b0381163014155b15612a4b57336000908152600260205260409020546001600160a01b031680612a045760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16612a495760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b038316612a715760405162461bcd60e51b815260040161068c906141dc565b6001600160a01b03808616600090815260066020908152604080832093881683529290522054612aa1908361304a565b6001600160a01b03868116600090815260066020908152604080832089851684529091528082209390935590851681522054612add908361306d565b6001600160a01b0380871660008181526006602090815260408083208986168085529252918290209490945551918716917f6eabe333476233fd382224f233210cb808a7bc4c4de64f9d76628bf63c677b1a90612b3b908790613efa565b60405180910390a45050505050565b6060856001600160401b0381118015612b6257600080fd5b50604051908082528060200260200182016040528015612b8c578160200160208202803683370190505b5090508560005b81811015612c5b576000878783818110612ba957fe5b905060200201359050620186a0612bca60328361327390919063ffffffff16565b81612bd157fe5b04848381518110612bde57fe5b602002602001018181525050612c528c8c84818110612bf957fe5b9050602002016020810190612c0e9190613522565b898985818110612c1a57fe5b905060200201358c8c86818110612c2d57fe5b9050602002016020810190612c429190613522565b6001600160a01b031691906132aa565b50600101612b93565b5060405163d9d1762360e01b81526001600160a01b038c169063d9d1762390612c969033908c908c908c908c908a908d908d90600401613cca565b600060405180830381600087803b158015612cb057600080fd5b505af1158015612cc4573d6000803e3d6000fd5b5050505060005b81811015611e27576000898983818110612ce157fe5b9050602002016020810190612cf69190613522565b9050612d3e612d17858481518110612d0a57fe5b6020026020010151612ec1565b6001600160a01b0383166000908152600760205260409020906001600160801b0316613189565b612d4782612fa2565b1015612d655760405162461bcd60e51b815260040161068c90614496565b8b8b83818110612d7157fe5b9050602002016020810190612d869190613522565b6001600160a01b0316816001600160a01b03168e6001600160a01b03167f3be9b85936d5d30a1655ea116a17ee3d827b2cd428cc026ce5bf2ac46a2232048b8b87818110612dd057fe5b90506020020135888781518110612de357fe5b6020026020010151604051612df99291906145f0565b60405180910390a450600101612ccb565b600660209081526000928352604080842090915290825290205481565b82516000906001600160801b0316612e40575081612eba565b835160208501516001600160801b0391821691612e5f91869116613273565b81612e6657fe5b049050818015612eaa57508284602001516001600160801b0316612ea086600001516001600160801b03168461327390919063ffffffff16565b81612ea757fe5b04105b15612eba576112ee81600161306d565b9392505050565b60006001600160801b03821115612eea5760405162461bcd60e51b815260040161068c90614137565b5090565b8181016001600160801b038083169082161015612f1d5760405162461bcd60e51b815260040161068c9061416e565b92915050565b600083602001516001600160801b031660001415612f42575081612eba565b602084015184516001600160801b0391821691612f6191869116613273565b81612f6857fe5b049050818015612eaa57508284600001516001600160801b0316612ea086602001516001600160801b03168461327390919063ffffffff16565b6001600160a01b0381166000818152600a60205260408082205490516370a0823160e01b81529192612f1d92600160801b9092046001600160801b0316916370a0823190612ff4903090600401613cb6565b60206040518083038186803b15801561300c57600080fd5b505afa158015613020573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906130449190613b0d565b9061306d565b80820382811115612f1d5760405162461bcd60e51b815260040161068c90613fa0565b81810181811015612f1d5760405162461bcd60e51b815260040161068c9061416e565b60006060856001600160a01b03166323b872dd60e01b8686866040516024016130bb93929190613dd7565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516130f99190613c70565b6000604051808303816000865af19150503d8060008114613136576040519150601f19603f3d011682016040523d82523d6000602084013e61313b565b606091505b50915091508180156131655750805115806131655750808060200190518101906131659190613707565b6131815760405162461bcd60e51b815260040161068c9061442a565b505050505050565b60006131a861319783612ec1565b84546001600160801b031690612eee565b83546001600160801b0319166001600160801b03919091169081179093555090919050565b60007f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a8667fd7df266aff736d415a9dc14b4158201d612e70d75b9c7f4e375ccfd20aa5166f83306040516020016132269493929190613f37565b6040516020818303038152906040528051906020012090505b919050565b8082036001600160801b038084169082161115612f1d5760405162461bcd60e51b815260040161068c90613fa0565b600081158061328e5750508082028282828161328b57fe5b04145b612f1d5760405162461bcd60e51b815260040161068c90614534565b60006060846001600160a01b031663a9059cbb60e01b85856040516024016132d3929190613e3c565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516133119190613c70565b6000604051808303816000865af19150503d806000811461334e576040519150601f19603f3d011682016040523d82523d6000602084013e613353565b606091505b509150915081801561337d57508051158061337d57508080602001905181019061337d9190613707565b6133995760405162461bcd60e51b815260040161068c90614074565b5050505050565b60006001600160401b03821115612eea5760405162461bcd60e51b815260040161068c906142e2565b60006131a86133d783612ec1565b84546001600160801b031690613244565b606060448251101561342e575060408051808201909152601d81527f5472616e73616374696f6e2072657665727465642073696c656e746c79000000602082015261323f565b60048201915081806020019051810190612f1d9190613b25565b604080518082019091526000808252602082015290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112613490578182fd5b5081356001600160401b038111156134a6578182fd5b60208301915083602080830285010111156134c057600080fd5b9250929050565b60008083601f8401126134d8578182fd5b5081356001600160401b038111156134ee578182fd5b6020830191508360208285010111156134c057600080fd5b8035612f1d816146be565b803560ff81168114612f1d57600080fd5b600060208284031215613533578081fd5b8135612eba816146be565b60008060408385031215613550578081fd5b823561355b816146be565b9150602083013561356b816146be565b809150509250929050565b60008060008060008060c0878903121561358e578182fd5b8635613599816146be565b955060208701356135a9816146be565b945060408701356135b9816146d6565b93506135c88860608901613511565b92506080870135915060a087013590509295509295509295565b600080604083850312156135f4578182fd5b82356135ff816146be565b9150602083013561356b816146d6565b600080600060608486031215613623578283fd5b833561362e816146be565b9250602084013561363e816146d6565b9150604084013561364e816146d6565b809150509250925092565b6000806000806060858703121561366e578384fd5b8435613679816146be565b935060208501356001600160401b03811115613693578384fd5b61369f878288016134c7565b90945092505060408501356136b3816146d6565b939692955090935050565b6000806000604084860312156136d2578081fd5b83356001600160401b038111156136e7578182fd5b6136f38682870161347f565b909450925050602084013561364e816146d6565b600060208284031215613718578081fd5b8151612eba816146d6565b600080600080600080600080600060a08a8c031215613740578687fd5b893561374b816146be565b985060208a01356001600160401b0380821115613766578889fd5b6137728d838e0161347f565b909a50985060408c013591508082111561378a578485fd5b6137968d838e0161347f565b909850965060608c01359150808211156137ae578485fd5b6137ba8d838e0161347f565b909650945060808c01359150808211156137d2578384fd5b506137df8c828d016134c7565b915080935050809150509295985092959850929598565b60008060408385031215613550578182fd5b6000806000806080858703121561381d578182fd5b8435613828816146be565b93506020850135613838816146be565b92506040850135613848816146be565b9396929550929360600135925050565b600080600080600060a0868803121561386f578283fd5b853561387a816146be565b9450602086013561388a816146be565b9350604086013561389a816146be565b94979396509394606081013594506080013592915050565b600080600080600080600080610100898b0312156138ce578182fd5b88356138d9816146be565b975060208901356138e9816146be565b965060408901356138f9816146be565b955060608901359450608089013593506139168a60a08b01613511565b925060c0890135915060e089013590509295985092959890939650565b6000806000806000806080878903121561394b578384fd5b8635613956816146be565b95506020870135613966816146be565b945060408701356001600160401b0380821115613981578586fd5b61398d8a838b0161347f565b909650945060608901359150808211156139a5578384fd5b506139b289828a0161347f565b979a9699509497509295939492505050565b6000806000606084860312156139d8578081fd5b83356139e3816146be565b925060208401356139f3816146d6565b929592945050506040919091013590565b60008060408385031215613a16578182fd5b8235613a21816146be565b946020939093013593505050565b600080600060608486031215613a43578081fd5b8335613a4e816146be565b925060208401359150604084013561364e816146d6565b60008060408385031215613a77578182fd5b8235613a82816146be565b915060208301356001600160401b038116811461356b578182fd5b60008060008060008060a08789031215613ab5578384fd5b8635613ac0816146be565b95506020870135613ad0816146be565b94506040870135613ae0816146be565b93506060870135925060808701356001600160401b03811115613b01578283fd5b6139b289828a016134c7565b600060208284031215613b1e578081fd5b5051919050565b600060208284031215613b36578081fd5b81516001600160401b0380821115613b4c578283fd5b818401915084601f830112613b5f578283fd5b815181811115613b6d578384fd5b604051601f8201601f191681016020018381118282101715613b8d578586fd5b604052818152838201602001871015613ba4578485fd5b613bb582602083016020870161468e565b9695505050505050565b6001600160a01b0316815260200190565b6000815180845260208085019450808401835b83811015613bff57815187529582019590820190600101613be3565b509495945050505050565b60008284528282602086013780602084860101526020601f19601f85011685010190509392505050565b60008151808452613c4c81602086016020860161468e565b601f01601f19169290920160200192915050565b6000828483379101908152919050565b60008251613c8281846020870161468e565b9190910192915050565b60008451613c9e81846020890161468e565b91909101928352506020820152604001919050565b90565b6001600160a01b0391909116815260200190565b6001600160a01b038916815260a0602080830182905260009183019081613cf18b82613efa565b90508b9250835b8b811015613d2357828401613d1683613d118388613506565b613bbf565b9094509150600101613cf8565b508481036040860152613d368982613efa565b9250506001600160fb1b03881115613d4c578283fd5b8702613d5981838b614682565b018281036060840152613d6c8187613bd0565b90508281036080840152613d81818587613c0a565b9b9a5050505050505050505050565b6001600160a01b03878116825286166020820152604081018590526060810184905260a060808201819052600090613dcb9083018486613c0a565b98975050505050505050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0397881681529590961660208601526040850193909352606084019190915260ff16608083015260a082015260c081019190915260e00190565b6001600160a01b03929092168252602082015260400190565b604080825283519082018190526000906020906060840190828701845b82811015613e90578151151584529284019290840190600101613e72565b50505083810382850152808551613ea78184613efa565b91508192508381028201848801865b83811015613ee0578583038552613ece838351613c34565b94870194925090860190600101613eb6565b50909998505050505050505050565b901515815260200190565b90815260200190565b95865260208601949094526001600160a01b039283166040860152911660608401521515608083015260a082015260c00190565b938452602084019290925260408301526001600160a01b0316606082015260800190565b93845260ff9290921660208401526040830152606082015260800190565b6000602082526112ee602083018486613c0a565b600060208252612eba6020830184613c34565b602080825260159082015274426f72696e674d6174683a20556e646572666c6f7760581b604082015260600190565b60208082526017908201527f42656e746f426f783a20536b696d20746f6f206d756368000000000000000000604082015260600190565b6020808252601c908201527f4d6173746572434d67723a2043616e6e6f7420617070726f7665203000000000604082015260600190565b6020808252601b908201527f4d6173746572434d67723a2075736572206e6f742073656e6465720000000000604082015260600190565b6020808252601c908201527f426f72696e6745524332303a205472616e73666572206661696c656400000000604082015260600190565b60208082526016908201527542656e746f426f783a2063616e6e6f7420656d70747960501b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b60208082526013908201527242656e746f426f783a204e6f20746f6b656e7360681b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b60208082526017908201527f42656e746f426f783a20746f5b305d206e6f7420736574000000000000000000604082015260600190565b60208082526014908201527310995b9d1bd09bde0e881d1bc81b9bdd081cd95d60621b604082015260600190565b6020808252601a908201527f53747261746567794d616e616765723a20546f6f206561726c79000000000000604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b6020808252601f908201527f42656e746f426f783a205472616e73666572206e6f7420617070726f76656400604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b6020808252601b908201527f426f72696e674d6174683a2075696e743634204f766572666c6f770000000000604082015260600190565b60208082526019908201527f4d6173746572434d67723a207573657220697320636c6f6e6500000000000000604082015260600190565b6020808252601d908201527f4d6173746572434d67723a20496e76616c6964205369676e6174757265000000604082015260600190565b6020808252818101527f426f72696e67466163746f72793a204e6f206d6173746572436f6e7472616374604082015260600190565b6020808252601b908201527f4d6173746572434d67723a206d617374657243206e6f74207365740000000000604082015260600190565b6020808252601b908201527f42656e746f426f783a206e6f206d6173746572436f6e74726163740000000000604082015260600190565b6020808252818101527f426f72696e6745524332303a205472616e7366657246726f6d206661696c6564604082015260600190565b6020808252601c908201527f4d6173746572434d67723a20557365722063616e6e6f74206265203000000000604082015260600190565b60208082526016908201527510995b9d1bd09bde0e8815dc9bdb99c8185b5bdd5b9d60521b604082015260600190565b6020808252601b908201527f4d6173746572434d67723a206e6f742077686974656c69737465640000000000604082015260600190565b6020808252601d908201527f42656e746f426f783a20455448207472616e73666572206661696c6564000000604082015260600190565b60208082526018908201527f426f72696e674d6174683a204d756c204f766572666c6f770000000000000000604082015260600190565b6020808252818101527f53747261746567794d616e616765723a2054617267657420746f6f2068696768604082015260600190565b6001600160801b0391909116815260200190565b6001600160801b039290921682526001600160a01b0316602082015260400190565b6001600160801b0392831681529116602082015260400190565b918252602082015260400190565b6001600160401b0391909116815260200190565b6001600160401b0393841681529190921660208201526001600160801b03909116604082015260600190565b6000808335601e19843603018112614654578283fd5b8301803591506001600160401b0382111561466d578283fd5b6020019150368190038213156134c057600080fd5b82818337506000910152565b60005b838110156146a9578181015183820152602001614691565b838111156146b8576000848401525b50505050565b6001600160a01b03811681146146d357600080fd5b50565b80151581146146d357600080fdfea264697066735822122073faf05d2f16c218afa386d66e9106020c7faffb68b3b44ad5bfabe7b878bc7264736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106101f25760003560e01c80637c516e941161010d578063c0a47c93116100a0578063e30c39781161006f578063e30c397814610596578063f1676d37146105ab578063f18d03cc146105cb578063f483b3da146105eb578063f7888aec1461060b576101f9565b8063c0a47c9314610506578063d2423b5114610526578063da5139ca14610547578063df23b45b14610567576101f9565b806397da6d30116100dc57806397da6d30146104915780639a7a4b5d146104b1578063aee4d1b2146104d1578063bafe4f14146104e6576101f9565b80637c516e941461041c5780637ecebe001461043c5780638da5cb5b1461045c57806391e0eab514610471576101f9565b80633e2a9d4e116101855780635662311811610154578063566231181461039c57806366c6bb0b146103bc57806372cb5d97146103dc578063733a9d7c146103fc576101f9565b80633e2a9d4e146103195780634e71e0c8146103395780634ffe34db1461034e5780635108a5581461037c576101f9565b80631583d56c116101c15780631583d56c146102975780631f54245b146102b7578063228bfd9f146102d75780633644e515146102f7576101f9565b806302b9446c146101fe578063078dfbe7146102285780630fca88431461024a57806312a90c8a1461026a576101f9565b366101f957005b600080fd5b61021161020c366004613858565b61062b565b60405161021f9291906145f0565b60405180910390f35b34801561023457600080fd5b5061024861024336600461360f565b610aef565b005b34801561025657600080fd5b50610248610265366004613933565b610bd5565b34801561027657600080fd5b5061028a610285366004613522565b610e86565b60405161021f9190613eef565b3480156102a357600080fd5b506102486102b2366004613a04565b610e9b565b6102ca6102c5366004613659565b610ed2565b60405161021f9190613cb6565b3480156102e357600080fd5b506102ca6102f2366004613522565b61108e565b34801561030357600080fd5b5061030c6110a9565b60405161021f9190613efa565b34801561032557600080fd5b50610248610334366004613a65565b611109565b34801561034557600080fd5b506102486111d4565b34801561035a57600080fd5b5061036e610369366004613522565b611261565b60405161021f9291906145d6565b34801561038857600080fd5b506102ca610397366004613522565b611287565b3480156103a857600080fd5b5061030c6103b7366004613a2f565b6112a2565b3480156103c857600080fd5b506102486103d73660046139c4565b6112f6565b3480156103e857600080fd5b506102486103f73660046137f6565b6118bf565b34801561040857600080fd5b506102486104173660046135e2565b611d1d565b34801561042857600080fd5b506102486104373660046138b2565b611dc1565b34801561044857600080fd5b5061030c610457366004613522565b611e35565b34801561046857600080fd5b506102ca611e47565b34801561047d57600080fd5b5061028a61048c36600461353e565b611e56565b34801561049d57600080fd5b506102116104ac366004613858565b611e76565b3480156104bd57600080fd5b506102486104cc366004613a04565b61228a565b3480156104dd57600080fd5b506102486122c0565b3480156104f257600080fd5b506102ca610501366004613522565b612307565b34801561051257600080fd5b50610248610521366004613576565b612322565b6105396105343660046136be565b612631565b60405161021f929190613e55565b34801561055357600080fd5b5061030c610562366004613a2f565b6127c1565b34801561057357600080fd5b50610587610582366004613522565b61280d565b60405161021f93929190614612565b3480156105a257600080fd5b506102ca612846565b3480156105b757600080fd5b506102486105c6366004613a9d565b612855565b3480156105d757600080fd5b506102486105e6366004613808565b6129a6565b3480156105f757600080fd5b50610248610606366004613723565b612b4a565b34801561061757600080fd5b5061030c6106263660046137f6565b612e0a565b600080856001600160a01b038116331480159061065157506001600160a01b0381163014155b156106dc57336000908152600260205260409020546001600160a01b0316806106955760405162461bcd60e51b815260040161068c906143f3565b60405180910390fd5b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff166106da5760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b0386166107025760405162461bcd60e51b815260040161068c906141dc565b60006001600160a01b03891615610719578861073b565b7f00000000000000000000000000000000000000000000000000000000000000005b9050610745613448565b506001600160a01b0381166000908152600760209081526040918290208251808401909352546001600160801b03808216808552600160801b909204169183019190915215158061080657506000826001600160a01b03166318160ddd6040518163ffffffff1660e01b815260040160206040518083038186803b1580156107cc57600080fd5b505afa1580156107e0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108049190613b0d565b115b6108225760405162461bcd60e51b815260040161068c9061410a565b8561087a5761083381886000612e27565b95506103e861085861084488612ec1565b60208401516001600160801b031690612eee565b6001600160801b0316101561087557600080945094505050610ae4565b610889565b61088681876001612f23565b96505b6001600160a01b038916301415806108a857506001600160a01b038a16155b806108d0575080516108cc906001600160801b03166108c684612fa2565b9061304a565b8711155b6108ec5760405162461bcd60e51b815260040161068c90613fcf565b6001600160a01b038083166000908152600660209081526040808320938c168352929052205461091c908761306d565b6001600160a01b038084166000908152600660209081526040808320938d168352929052205561096261094e87612ec1565b60208301516001600160801b031690612eee565b6001600160801b0316602082015261098d61097c88612ec1565b82516001600160801b031690612eee565b6001600160801b0390811682526001600160a01b03808416600090815260076020908152604090912084518154928601518516600160801b029085166001600160801b031990931692909217909316179091558a16610a5f577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663d0e30db0886040518263ffffffff1660e01b81526004016000604051808303818588803b158015610a4157600080fd5b505af1158015610a55573d6000803e3d6000fd5b5050505050610a84565b6001600160a01b0389163014610a8457610a846001600160a01b0383168a308a613090565b876001600160a01b0316896001600160a01b0316836001600160a01b03167fb2346165e782564f17f5b7e555c21f4fd96fbc93458572bf0113ea35a958fc558a8a604051610ad39291906145f0565b60405180910390a486945085935050505b509550959350505050565b6000546001600160a01b03163314610b195760405162461bcd60e51b815260040161068c90614241565b8115610bb4576001600160a01b038316151580610b335750805b610b4f5760405162461bcd60e51b815260040161068c906140db565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b031991821617909155600180549091169055610bd0565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b846001600160a01b0381163314801590610bf857506001600160a01b0381163014155b15610c7a57336000908152600260205260409020546001600160a01b031680610c335760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16610c785760405162461bcd60e51b815260040161068c90614276565b505b600085858281610c8657fe5b9050602002016020810190610c9b9190613522565b6001600160a01b03161415610cc25760405162461bcd60e51b815260040161068c906141a5565b600084815b81811015610e1e576000888883818110610cdd57fe5b9050602002016020810190610cf29190613522565b9050610d61878784818110610d0357fe5b90506020020135600660008e6001600160a01b03166001600160a01b031681526020019081526020016000206000846001600160a01b03166001600160a01b031681526020019081526020016000205461306d90919063ffffffff16565b6001600160a01b03808d16600090815260066020908152604080832093861683529290522055610dac878784818110610d9657fe5b905060200201358561306d90919063ffffffff16565b9350806001600160a01b03168a6001600160a01b03168c6001600160a01b03167f6eabe333476233fd382224f233210cb808a7bc4c4de64f9d76628bf63c677b1a8a8a87818110610df957fe5b90506020020135604051610e0d9190613efa565b60405180910390a450600101610cc7565b506001600160a01b03808a166000908152600660209081526040808320938c1683529290522054610e4f908361304a565b6001600160a01b03998a1660009081526006602090815260408083209b909c16825299909952989097209790975550505050505050565b60046020526000908152604090205460ff1681565b610eb06001600160a01b038316333084613090565b6001600160a01b0382166000908152600760205260409020610bd09082613189565b60006001600160a01b038516610efa5760405162461bcd60e51b815260040161068c90614387565b606085901b8215610f6c5760008585604051610f17929190613c60565b60405180910390209050604051733d602d80600a3d3981f3363d3d373d3d3d363d7360601b81528260148201526e5af43d82803e903d91602b57fd5bf360881b6028820152816037826000f593505050610fb1565b604051733d602d80600a3d3981f3363d3d373d3d3d363d7360601b81528160148201526e5af43d82803e903d91602b57fd5bf360881b60288201526037816000f09250505b6001600160a01b038281166000818152600260205260409081902080546001600160a01b031916938a16939093179092559051631377d1f560e21b8152634ddf47d49034906110069089908990600401613f79565b6000604051808303818588803b15801561101f57600080fd5b505af1158015611033573d6000803e3d6000fd5b5050505050816001600160a01b0316866001600160a01b03167fd62166f3c2149208e51788b1401cc356bf5da1fc6c7886a32e18570f57d88b3b878760405161107d929190613f79565b60405180910390a350949350505050565b6008602052600090815260409020546001600160a01b031681565b6000467f000000000000000000000000000000000000000000000000000000000000000081146110e1576110dc816131cd565b611103565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b6000546001600160a01b031633146111335760405162461bcd60e51b815260040161068c90614241565b605f816001600160401b0316111561115d5760405162461bcd60e51b815260040161068c9061456b565b6001600160a01b0382166000818152600a602052604090819020805467ffffffffffffffff60401b1916600160401b6001600160401b03861602179055517f7543af99b5602c06e62da0631b5308489a5ff859150105a623b6eb15e8deae0b906111c89084906145fe565b60405180910390a25050565b6001546001600160a01b03163381146111ff5760405162461bcd60e51b815260040161068c906142ad565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b6007602052600090815260409020546001600160801b0380821691600160801b90041682565b6009602052600090815260409020546001600160a01b031681565b6001600160a01b03831660009081526007602090815260408083208151808301909252546001600160801b038082168352600160801b90910416918101919091526112ee908484612f23565b949350505050565b6112fe61345f565b506001600160a01b038381166000818152600a60209081526040808320815160608101835290546001600160401b038082168352600160401b82041682850152600160801b90046001600160801b031681830190815294845260089092528083205493519051630c7e663b60e11b81529194939093169283916318fccc769161138b9133906004016145b4565b602060405180830381600087803b1580156113a557600080fd5b505af11580156113b9573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906113dd9190613b0d565b9050801580156113eb575084155b156113f857505050610bd0565b6001600160a01b0386166000908152600760205260408120546001600160801b0316908213156114ae578161142d828261306d565b915061143882612ec1565b6001600160a01b0389166000818152600760205260409081902080546001600160801b0319166001600160801b03949094169390931790925590517f911c9f20a03edabcbcbd18dca1174cce47a91b234ced7a5a3c60ba0d5b56c5d2906114a0908490613efa565b60405180910390a25061157c565b600082121561157c5760008290036114c6828261304a565b91506114d182612ec1565b6001600160a01b038916600090815260076020526040902080546001600160801b0319166001600160801b039290921691909117905561152761151382612ec1565b60408701516001600160801b031690613244565b6001600160801b0316604080870191909152516001600160a01b038916907f8f1f26eb9b6aa8689dbdd519ead1999d9c8819d4738e403b2003b18197d9cf9790611572908490613efa565b60405180910390a2505b851561183b57600060646115a686602001516001600160401b03168461327390919063ffffffff16565b816115ad57fe5b0490508085604001516001600160801b031610156116e85760006115e786604001516001600160801b03168361304a90919063ffffffff16565b905086158015906115f757508681115b156115ff5750855b6116136001600160a01b038a1686836132aa565b61163361161f82612ec1565b60408801516001600160801b031690612eee565b6001600160801b031660408088019190915251636939aaf560e01b81526001600160a01b03861690636939aaf59061166f908490600401613efa565b600060405180830381600087803b15801561168957600080fd5b505af115801561169d573d6000803e3d6000fd5b50505050886001600160a01b03167fb18e7e4f6eac147a63a3bb6beb2d9039c88698623aff3efc4febbc20b0164ee5826040516116da9190613efa565b60405180910390a250611839565b8085604001516001600160801b0316111561183957600061171f61170b83612ec1565b60408801516001600160801b031690613244565b6001600160801b03169050861580159061173857508681115b156117405750855b604051632e1a7d4d60e01b81526000906001600160a01b03871690632e1a7d4d9061176f908590600401613efa565b602060405180830381600087803b15801561178957600080fd5b505af115801561179d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117c19190613b0d565b90506117e36117cf82612ec1565b60408901516001600160801b031690613244565b6001600160801b0316604080890191909152516001600160a01b038b16907f39aa22060f8dd4d291720311feedf3b72fef47c06c66ccf5c22b502c62e7550a9061182e908490613efa565b60405180910390a250505b505b5050506001600160a01b0384166000908152600a6020908152604091829020835181549285015193909401516001600160801b03908116600160801b026001600160401b03948516600160401b0267ffffffffffffffff60401b199590961667ffffffffffffffff1990941693909317939093169390931791909116179055505050565b6000546001600160a01b031633146118e95760405162461bcd60e51b815260040161068c90614241565b6118f161345f565b506001600160a01b038281166000818152600a60209081526040808320815160608101835290546001600160401b038082168352600160401b8204811683860152600160801b9091046001600160801b0316828401529484526009909252909120548151919316911615806119785750826001600160a01b0316816001600160a01b031614155b15611a02576001600160a01b03848116600090815260096020526040902080546001600160a01b0319169185169190911790556119b96212750042016133a0565b6001600160401b031682526040516001600160a01b0380851691908616907f6f7ccdf3f86039e5a1dcf6028bf7b4773cbf7a234716ba2e5392b12bb0f8558f90600090a3611c9d565b81516001600160401b031615801590611a25575081516001600160401b03164210155b611a415760405162461bcd60e51b815260040161068c9061420a565b6001600160a01b038481166000908152600860205260409020541615611c26576001600160a01b0380851660009081526008602052604080822054858201519151637f8661a160e01b815292931691637f8661a191611aa2916004016145a0565b602060405180830381600087803b158015611abc57600080fd5b505af1158015611ad0573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611af49190613b0d565b90506000811315611b6b576001600160a01b03851660009081526007602052604090208190611b239082613189565b50856001600160a01b03167f911c9f20a03edabcbcbd18dca1174cce47a91b234ced7a5a3c60ba0d5b56c5d282604051611b5d9190613efa565b60405180910390a250611bdf565b6000811215611bdf576001600160a01b03851660009081526007602052604081209082900390611b9b90826133c9565b50856001600160a01b03167f8f1f26eb9b6aa8689dbdd519ead1999d9c8819d4738e403b2003b18197d9cf9782604051611bd59190613efa565b60405180910390a2505b846001600160a01b03167f39aa22060f8dd4d291720311feedf3b72fef47c06c66ccf5c22b502c62e7550a8460400151604051611c1c91906145a0565b60405180910390a2505b6001600160a01b03808516600081815260086020908152604080832080548688166001600160a01b0319918216179091558388528782018490528484526009909252808320805490921690915551928616927f03e6352a885adc4cc54767592939c3b1bbd65685658c3beaaba66a888120e2179190a35b506001600160a01b03929092166000908152600a60209081526040918290208451815492860151939095015167ffffffffffffffff199092166001600160401b039586161767ffffffffffffffff60401b1916600160401b9590931694909402919091176001600160801b03908116600160801b91909216021790915550565b6000546001600160a01b03163314611d475760405162461bcd60e51b815260040161068c90614241565b6001600160a01b038216611d6d5760405162461bcd60e51b815260040161068c90614006565b6001600160a01b03821660008181526004602052604090819020805460ff1916841515179055517f31a1e0eac44b54ac6c2a2efa87e92c83405ffcf33fceef02a7bca695130e2600906111c8908490613eef565b60405163d505accf60e01b81526001600160a01b0389169063d505accf90611df9908a908a908a908a908a908a908a90600401613dfb565b600060405180830381600087803b158015611e1357600080fd5b505af1158015611e27573d6000803e3d6000fd5b505050505050505050505050565b60056020526000908152604090205481565b6000546001600160a01b031681565b600360209081526000928352604080842090915290825290205460ff1681565b600080856001600160a01b0381163314801590611e9c57506001600160a01b0381163014155b15611f1e57336000908152600260205260409020546001600160a01b031680611ed75760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16611f1c5760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b038616611f445760405162461bcd60e51b815260040161068c906141dc565b60006001600160a01b03891615611f5b5788611f7d565b7f00000000000000000000000000000000000000000000000000000000000000005b9050611f87613448565b506001600160a01b0381166000908152600760209081526040918290208251808401909352546001600160801b038082168452600160801b909104169082015285611fdf57611fd881886001612e27565b9550611fee565b611feb81876000612f23565b96505b6001600160a01b038083166000908152600660209081526040808320938d168352929052205461201e908761304a565b6001600160a01b038084166000908152600660209081526040808320938e168352929052205561206161205088612ec1565b82516001600160801b031690613244565b6001600160801b0316815261208c61207887612ec1565b60208301516001600160801b031690613244565b6001600160801b0316602082018190526103e81115806120b7575060208101516001600160801b0316155b6120d35760405162461bcd60e51b815260040161068c906140ab565b6001600160a01b03828116600090815260076020908152604090912083518154928501516001600160801b03199093166001600160801b03918216178116600160801b91909316029190911790558a1661222757604051632e1a7d4d60e01b81526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690632e1a7d4d90612173908a90600401613efa565b600060405180830381600087803b15801561218d57600080fd5b505af11580156121a1573d6000803e3d6000fd5b505050506000886001600160a01b0316886040516121be90613cb3565b60006040518083038185875af1925050503d80600081146121fb576040519150601f19603f3d011682016040523d82523d6000602084013e612200565b606091505b50509050806122215760405162461bcd60e51b815260040161068c906144fd565b5061223b565b61223b6001600160a01b03831689896132aa565b876001600160a01b0316896001600160a01b0316836001600160a01b03167fad9ab9ee6953d4d177f4a03b3a3ac3178ffcb9816319f348060194aa76b144868a8a604051610ad39291906145f0565b61229e6001600160a01b03831633836132aa565b6001600160a01b0382166000908152600760205260409020610bd090826133c9565b3360008181526002602052604080822080546001600160a01b03191684179055517fdfb44ffabf0d3a8f650d3ce43eff98f6d050e7ea1a396d5794f014e7dadabacb9190a2565b6002602052600090815260409020546001600160a01b031681565b6001600160a01b0385166123485760405162461bcd60e51b815260040161068c906143bc565b81158015612354575080155b8015612361575060ff8316155b15612403576001600160a01b038616331461238e5760405162461bcd60e51b815260040161068c9061403d565b6001600160a01b0386811660009081526002602052604090205416156123c65760405162461bcd60e51b815260040161068c90614319565b6001600160a01b03851660009081526004602052604090205460ff166123fe5760405162461bcd60e51b815260040161068c906144c6565b6125bd565b6001600160a01b0386166124295760405162461bcd60e51b815260040161068c9061445f565b600060405180604001604052806002815260200161190160f01b81525061244e6110a9565b7f1962bc9f5484cb7a998701b81090e966ee1fce5771af884cceee7c081b14ade28761249a577fb426802f1f7dc850a7b6b38805edea2442f3992878a9ab985abfe8091d95d0b16124bc565b7f422ac5323fe049241dee67716229a1cc1bc7b313b23dfe3ef6d42ab177a3b2845b6001600160a01b038b1660009081526005602090815260409182902080546001810190915591516124f69493928e928e928e929101613f03565b6040516020818303038152906040528051906020012060405160200161251e93929190613c8c565b60405160208183030381529060405280519060200120905060006001828686866040516000815260200160405260405161255b9493929190613f5b565b6020604051602081039080840390855afa15801561257d573d6000803e3d6000fd5b505050602060405103519050876001600160a01b0316816001600160a01b0316146125ba5760405162461bcd60e51b815260040161068c90614350565b50505b6001600160a01b038581166000818152600360209081526040808320948b168084529490915290819020805460ff1916881515179055517f5f6ebb64ba012a851c6f014e6cad458ddf213d1512049b31cd06365c2b05925790612621908890613eef565b60405180910390a3505050505050565b606080836001600160401b038111801561264a57600080fd5b50604051908082528060200260200182016040528015612674578160200160208202803683370190505b509150836001600160401b038111801561268d57600080fd5b506040519080825280602002602001820160405280156126c157816020015b60608152602001906001900390816126ac5790505b50905060005b848110156127b85760006060308888858181106126e057fe5b90506020028101906126f2919061463e565b604051612700929190613c60565b600060405180830381855af49150503d806000811461273b576040519150601f19603f3d011682016040523d82523d6000602084013e612740565b606091505b5091509150818061274f575085155b612758826133e8565b906127765760405162461bcd60e51b815260040161068c9190613f8d565b508185848151811061278457fe5b602002602001019015159081151581525050808484815181106127a357fe5b602090810291909101015250506001016126c7565b50935093915050565b6001600160a01b03831660009081526007602090815260408083208151808301909252546001600160801b038082168352600160801b90910416918101919091526112ee908484612e27565b600a602052600090815260409020546001600160401b0380821691600160401b810490911690600160801b90046001600160801b031683565b6001546001600160a01b031681565b6000620186a0612866856032613273565b8161286d57fe5b0490506128846001600160a01b03861687866132aa565b6040516323e30c8b60e01b81526001600160a01b038816906323e30c8b906128ba9033908990899087908a908a90600401613d90565b600060405180830381600087803b1580156128d457600080fd5b505af11580156128e8573d6000803e3d6000fd5b5050505061291f6128f882612ec1565b6001600160a01b0387166000908152600760205260409020906001600160801b0316613189565b61292886612fa2565b10156129465760405162461bcd60e51b815260040161068c90614496565b856001600160a01b0316856001600160a01b0316886001600160a01b03167f3be9b85936d5d30a1655ea116a17ee3d827b2cd428cc026ce5bf2ac46a22320487856040516129959291906145f0565b60405180910390a450505050505050565b826001600160a01b03811633148015906129c957506001600160a01b0381163014155b15612a4b57336000908152600260205260409020546001600160a01b031680612a045760405162461bcd60e51b815260040161068c906143f3565b6001600160a01b0380821660009081526003602090815260408083209386168352929052205460ff16612a495760405162461bcd60e51b815260040161068c90614276565b505b6001600160a01b038316612a715760405162461bcd60e51b815260040161068c906141dc565b6001600160a01b03808616600090815260066020908152604080832093881683529290522054612aa1908361304a565b6001600160a01b03868116600090815260066020908152604080832089851684529091528082209390935590851681522054612add908361306d565b6001600160a01b0380871660008181526006602090815260408083208986168085529252918290209490945551918716917f6eabe333476233fd382224f233210cb808a7bc4c4de64f9d76628bf63c677b1a90612b3b908790613efa565b60405180910390a45050505050565b6060856001600160401b0381118015612b6257600080fd5b50604051908082528060200260200182016040528015612b8c578160200160208202803683370190505b5090508560005b81811015612c5b576000878783818110612ba957fe5b905060200201359050620186a0612bca60328361327390919063ffffffff16565b81612bd157fe5b04848381518110612bde57fe5b602002602001018181525050612c528c8c84818110612bf957fe5b9050602002016020810190612c0e9190613522565b898985818110612c1a57fe5b905060200201358c8c86818110612c2d57fe5b9050602002016020810190612c429190613522565b6001600160a01b031691906132aa565b50600101612b93565b5060405163d9d1762360e01b81526001600160a01b038c169063d9d1762390612c969033908c908c908c908c908a908d908d90600401613cca565b600060405180830381600087803b158015612cb057600080fd5b505af1158015612cc4573d6000803e3d6000fd5b5050505060005b81811015611e27576000898983818110612ce157fe5b9050602002016020810190612cf69190613522565b9050612d3e612d17858481518110612d0a57fe5b6020026020010151612ec1565b6001600160a01b0383166000908152600760205260409020906001600160801b0316613189565b612d4782612fa2565b1015612d655760405162461bcd60e51b815260040161068c90614496565b8b8b83818110612d7157fe5b9050602002016020810190612d869190613522565b6001600160a01b0316816001600160a01b03168e6001600160a01b03167f3be9b85936d5d30a1655ea116a17ee3d827b2cd428cc026ce5bf2ac46a2232048b8b87818110612dd057fe5b90506020020135888781518110612de357fe5b6020026020010151604051612df99291906145f0565b60405180910390a450600101612ccb565b600660209081526000928352604080842090915290825290205481565b82516000906001600160801b0316612e40575081612eba565b835160208501516001600160801b0391821691612e5f91869116613273565b81612e6657fe5b049050818015612eaa57508284602001516001600160801b0316612ea086600001516001600160801b03168461327390919063ffffffff16565b81612ea757fe5b04105b15612eba576112ee81600161306d565b9392505050565b60006001600160801b03821115612eea5760405162461bcd60e51b815260040161068c90614137565b5090565b8181016001600160801b038083169082161015612f1d5760405162461bcd60e51b815260040161068c9061416e565b92915050565b600083602001516001600160801b031660001415612f42575081612eba565b602084015184516001600160801b0391821691612f6191869116613273565b81612f6857fe5b049050818015612eaa57508284600001516001600160801b0316612ea086602001516001600160801b03168461327390919063ffffffff16565b6001600160a01b0381166000818152600a60205260408082205490516370a0823160e01b81529192612f1d92600160801b9092046001600160801b0316916370a0823190612ff4903090600401613cb6565b60206040518083038186803b15801561300c57600080fd5b505afa158015613020573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906130449190613b0d565b9061306d565b80820382811115612f1d5760405162461bcd60e51b815260040161068c90613fa0565b81810181811015612f1d5760405162461bcd60e51b815260040161068c9061416e565b60006060856001600160a01b03166323b872dd60e01b8686866040516024016130bb93929190613dd7565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516130f99190613c70565b6000604051808303816000865af19150503d8060008114613136576040519150601f19603f3d011682016040523d82523d6000602084013e61313b565b606091505b50915091508180156131655750805115806131655750808060200190518101906131659190613707565b6131815760405162461bcd60e51b815260040161068c9061442a565b505050505050565b60006131a861319783612ec1565b84546001600160801b031690612eee565b83546001600160801b0319166001600160801b03919091169081179093555090919050565b60007f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a8667fd7df266aff736d415a9dc14b4158201d612e70d75b9c7f4e375ccfd20aa5166f83306040516020016132269493929190613f37565b6040516020818303038152906040528051906020012090505b919050565b8082036001600160801b038084169082161115612f1d5760405162461bcd60e51b815260040161068c90613fa0565b600081158061328e5750508082028282828161328b57fe5b04145b612f1d5760405162461bcd60e51b815260040161068c90614534565b60006060846001600160a01b031663a9059cbb60e01b85856040516024016132d3929190613e3c565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b03199094169390931790925290516133119190613c70565b6000604051808303816000865af19150503d806000811461334e576040519150601f19603f3d011682016040523d82523d6000602084013e613353565b606091505b509150915081801561337d57508051158061337d57508080602001905181019061337d9190613707565b6133995760405162461bcd60e51b815260040161068c90614074565b5050505050565b60006001600160401b03821115612eea5760405162461bcd60e51b815260040161068c906142e2565b60006131a86133d783612ec1565b84546001600160801b031690613244565b606060448251101561342e575060408051808201909152601d81527f5472616e73616374696f6e2072657665727465642073696c656e746c79000000602082015261323f565b60048201915081806020019051810190612f1d9190613b25565b604080518082019091526000808252602082015290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112613490578182fd5b5081356001600160401b038111156134a6578182fd5b60208301915083602080830285010111156134c057600080fd5b9250929050565b60008083601f8401126134d8578182fd5b5081356001600160401b038111156134ee578182fd5b6020830191508360208285010111156134c057600080fd5b8035612f1d816146be565b803560ff81168114612f1d57600080fd5b600060208284031215613533578081fd5b8135612eba816146be565b60008060408385031215613550578081fd5b823561355b816146be565b9150602083013561356b816146be565b809150509250929050565b60008060008060008060c0878903121561358e578182fd5b8635613599816146be565b955060208701356135a9816146be565b945060408701356135b9816146d6565b93506135c88860608901613511565b92506080870135915060a087013590509295509295509295565b600080604083850312156135f4578182fd5b82356135ff816146be565b9150602083013561356b816146d6565b600080600060608486031215613623578283fd5b833561362e816146be565b9250602084013561363e816146d6565b9150604084013561364e816146d6565b809150509250925092565b6000806000806060858703121561366e578384fd5b8435613679816146be565b935060208501356001600160401b03811115613693578384fd5b61369f878288016134c7565b90945092505060408501356136b3816146d6565b939692955090935050565b6000806000604084860312156136d2578081fd5b83356001600160401b038111156136e7578182fd5b6136f38682870161347f565b909450925050602084013561364e816146d6565b600060208284031215613718578081fd5b8151612eba816146d6565b600080600080600080600080600060a08a8c031215613740578687fd5b893561374b816146be565b985060208a01356001600160401b0380821115613766578889fd5b6137728d838e0161347f565b909a50985060408c013591508082111561378a578485fd5b6137968d838e0161347f565b909850965060608c01359150808211156137ae578485fd5b6137ba8d838e0161347f565b909650945060808c01359150808211156137d2578384fd5b506137df8c828d016134c7565b915080935050809150509295985092959850929598565b60008060408385031215613550578182fd5b6000806000806080858703121561381d578182fd5b8435613828816146be565b93506020850135613838816146be565b92506040850135613848816146be565b9396929550929360600135925050565b600080600080600060a0868803121561386f578283fd5b853561387a816146be565b9450602086013561388a816146be565b9350604086013561389a816146be565b94979396509394606081013594506080013592915050565b600080600080600080600080610100898b0312156138ce578182fd5b88356138d9816146be565b975060208901356138e9816146be565b965060408901356138f9816146be565b955060608901359450608089013593506139168a60a08b01613511565b925060c0890135915060e089013590509295985092959890939650565b6000806000806000806080878903121561394b578384fd5b8635613956816146be565b95506020870135613966816146be565b945060408701356001600160401b0380821115613981578586fd5b61398d8a838b0161347f565b909650945060608901359150808211156139a5578384fd5b506139b289828a0161347f565b979a9699509497509295939492505050565b6000806000606084860312156139d8578081fd5b83356139e3816146be565b925060208401356139f3816146d6565b929592945050506040919091013590565b60008060408385031215613a16578182fd5b8235613a21816146be565b946020939093013593505050565b600080600060608486031215613a43578081fd5b8335613a4e816146be565b925060208401359150604084013561364e816146d6565b60008060408385031215613a77578182fd5b8235613a82816146be565b915060208301356001600160401b038116811461356b578182fd5b60008060008060008060a08789031215613ab5578384fd5b8635613ac0816146be565b95506020870135613ad0816146be565b94506040870135613ae0816146be565b93506060870135925060808701356001600160401b03811115613b01578283fd5b6139b289828a016134c7565b600060208284031215613b1e578081fd5b5051919050565b600060208284031215613b36578081fd5b81516001600160401b0380821115613b4c578283fd5b818401915084601f830112613b5f578283fd5b815181811115613b6d578384fd5b604051601f8201601f191681016020018381118282101715613b8d578586fd5b604052818152838201602001871015613ba4578485fd5b613bb582602083016020870161468e565b9695505050505050565b6001600160a01b0316815260200190565b6000815180845260208085019450808401835b83811015613bff57815187529582019590820190600101613be3565b509495945050505050565b60008284528282602086013780602084860101526020601f19601f85011685010190509392505050565b60008151808452613c4c81602086016020860161468e565b601f01601f19169290920160200192915050565b6000828483379101908152919050565b60008251613c8281846020870161468e565b9190910192915050565b60008451613c9e81846020890161468e565b91909101928352506020820152604001919050565b90565b6001600160a01b0391909116815260200190565b6001600160a01b038916815260a0602080830182905260009183019081613cf18b82613efa565b90508b9250835b8b811015613d2357828401613d1683613d118388613506565b613bbf565b9094509150600101613cf8565b508481036040860152613d368982613efa565b9250506001600160fb1b03881115613d4c578283fd5b8702613d5981838b614682565b018281036060840152613d6c8187613bd0565b90508281036080840152613d81818587613c0a565b9b9a5050505050505050505050565b6001600160a01b03878116825286166020820152604081018590526060810184905260a060808201819052600090613dcb9083018486613c0a565b98975050505050505050565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0397881681529590961660208601526040850193909352606084019190915260ff16608083015260a082015260c081019190915260e00190565b6001600160a01b03929092168252602082015260400190565b604080825283519082018190526000906020906060840190828701845b82811015613e90578151151584529284019290840190600101613e72565b50505083810382850152808551613ea78184613efa565b91508192508381028201848801865b83811015613ee0578583038552613ece838351613c34565b94870194925090860190600101613eb6565b50909998505050505050505050565b901515815260200190565b90815260200190565b95865260208601949094526001600160a01b039283166040860152911660608401521515608083015260a082015260c00190565b938452602084019290925260408301526001600160a01b0316606082015260800190565b93845260ff9290921660208401526040830152606082015260800190565b6000602082526112ee602083018486613c0a565b600060208252612eba6020830184613c34565b602080825260159082015274426f72696e674d6174683a20556e646572666c6f7760581b604082015260600190565b60208082526017908201527f42656e746f426f783a20536b696d20746f6f206d756368000000000000000000604082015260600190565b6020808252601c908201527f4d6173746572434d67723a2043616e6e6f7420617070726f7665203000000000604082015260600190565b6020808252601b908201527f4d6173746572434d67723a2075736572206e6f742073656e6465720000000000604082015260600190565b6020808252601c908201527f426f72696e6745524332303a205472616e73666572206661696c656400000000604082015260600190565b60208082526016908201527542656e746f426f783a2063616e6e6f7420656d70747960501b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b60208082526013908201527242656e746f426f783a204e6f20746f6b656e7360681b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b60208082526017908201527f42656e746f426f783a20746f5b305d206e6f7420736574000000000000000000604082015260600190565b60208082526014908201527310995b9d1bd09bde0e881d1bc81b9bdd081cd95d60621b604082015260600190565b6020808252601a908201527f53747261746567794d616e616765723a20546f6f206561726c79000000000000604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b6020808252601f908201527f42656e746f426f783a205472616e73666572206e6f7420617070726f76656400604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b6020808252601b908201527f426f72696e674d6174683a2075696e743634204f766572666c6f770000000000604082015260600190565b60208082526019908201527f4d6173746572434d67723a207573657220697320636c6f6e6500000000000000604082015260600190565b6020808252601d908201527f4d6173746572434d67723a20496e76616c6964205369676e6174757265000000604082015260600190565b6020808252818101527f426f72696e67466163746f72793a204e6f206d6173746572436f6e7472616374604082015260600190565b6020808252601b908201527f4d6173746572434d67723a206d617374657243206e6f74207365740000000000604082015260600190565b6020808252601b908201527f42656e746f426f783a206e6f206d6173746572436f6e74726163740000000000604082015260600190565b6020808252818101527f426f72696e6745524332303a205472616e7366657246726f6d206661696c6564604082015260600190565b6020808252601c908201527f4d6173746572434d67723a20557365722063616e6e6f74206265203000000000604082015260600190565b60208082526016908201527510995b9d1bd09bde0e8815dc9bdb99c8185b5bdd5b9d60521b604082015260600190565b6020808252601b908201527f4d6173746572434d67723a206e6f742077686974656c69737465640000000000604082015260600190565b6020808252601d908201527f42656e746f426f783a20455448207472616e73666572206661696c6564000000604082015260600190565b60208082526018908201527f426f72696e674d6174683a204d756c204f766572666c6f770000000000000000604082015260600190565b6020808252818101527f53747261746567794d616e616765723a2054617267657420746f6f2068696768604082015260600190565b6001600160801b0391909116815260200190565b6001600160801b039290921682526001600160a01b0316602082015260400190565b6001600160801b0392831681529116602082015260400190565b918252602082015260400190565b6001600160401b0391909116815260200190565b6001600160401b0393841681529190921660208201526001600160801b03909116604082015260600190565b6000808335601e19843603018112614654578283fd5b8301803591506001600160401b0382111561466d578283fd5b6020019150368190038213156134c057600080fd5b82818337506000910152565b60005b838110156146a9578181015183820152602001614691565b838111156146b8576000848401525b50505050565b6001600160a01b03811681146146d357600080fd5b50565b80151581146146d357600080fdfea264697066735822122073faf05d2f16c218afa386d66e9106020c7faffb68b3b44ad5bfabe7b878bc7264736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": { + "batch(bytes[],bool)": { + "params": { + "calls": "An array of inputs for each call.", + "revertOnFail": "If True then reverts after a failed call and stops doing further calls." + }, + "returns": { + "results": "An array with the returned data of each function call, mapped one-to-one to `calls`.", + "successes": "An array indicating the success of a call, mapped one-to-one to `calls`." + } + }, + "batchFlashLoan(address,address[],address[],uint256[],bytes)": { + "params": { + "amounts": "of the tokens for each receiver.", + "borrower": "The address of the contract that implements and conforms to `IBatchFlashBorrower` and handles the flashloan.", + "data": "The calldata to pass to the `borrower` contract.", + "receivers": "An array of the token receivers. A one-to-one mapping with `tokens` and `amounts`.", + "tokens": "The addresses of the tokens." + } + }, + "deploy(address,bytes,bool)": { + "params": { + "data": "Additional abi encoded calldata that is passed to the new clone via `IMasterContract.init`.", + "masterContract": "The address of the contract to clone.", + "useCreate2": "Creates the clone by using the CREATE2 opcode, in this case `data` will be used as salt." + }, + "returns": { + "cloneAddress": "Address of the created clone contract." + } + }, + "deposit(address,address,address,uint256,uint256)": { + "params": { + "amount": "Token amount in native representation to deposit.", + "from": "which account to pull the tokens.", + "share": "Token amount represented in shares to deposit. Takes precedence over `amount`.", + "to": "which account to push the tokens.", + "token_": "The ERC-20 token to deposit." + }, + "returns": { + "amountOut": "The amount deposited.", + "shareOut": "The deposited amount repesented in shares." + } + }, + "flashLoan(address,address,address,uint256,bytes)": { + "params": { + "amount": "of the tokens to receive.", + "borrower": "The address of the contract that implements and conforms to `IFlashBorrower` and handles the flashloan.", + "data": "The calldata to pass to the `borrower` contract.", + "receiver": "Address of the token receiver.", + "token": "The address of the token to receive." + } + }, + "harvest(address,bool,uint256)": { + "params": { + "balance": "True if housekeeping should be done.", + "maxChangeAmount": "The maximum amount for either pulling or pushing from/to the `IStrategy` contract.", + "token": "The address of the token for which a strategy is deployed." + } + }, + "setMasterContractApproval(address,address,bool,uint8,bytes32,bytes32)": { + "params": { + "approved": "If True approves access. If False revokes access.", + "masterContract": "The address who gains or loses access.", + "r": "Part of the signature. (See EIP-191)", + "s": "Part of the signature. (See EIP-191)", + "user": "The address of the user that approves or revokes access.", + "v": "Part of the signature. (See EIP-191)" + } + }, + "setStrategy(address,address)": { + "details": "Only the owner of this contract is allowed to change this.", + "params": { + "newStrategy": "The address of the contract that conforms to `IStrategy`.", + "token": "The address of the token that maps to a strategy to change." + } + }, + "setStrategyTargetPercentage(address,uint64)": { + "details": "Only the owner of this contract is allowed to change this.", + "params": { + "targetPercentage_": "The new target in percent. Must be lesser or equal to `MAX_TARGET_PERCENTAGE`.", + "token": "The address of the token that maps to a strategy to change." + } + }, + "toAmount(address,uint256,bool)": { + "details": "Helper function represent shares back into the `token` amount.", + "params": { + "roundUp": "If the result should be rounded up.", + "share": "The amount of shares.", + "token": "The ERC-20 token." + }, + "returns": { + "amount": "The share amount back into native representation." + } + }, + "toShare(address,uint256,bool)": { + "details": "Helper function to represent an `amount` of `token` in shares.", + "params": { + "amount": "The `token` amount.", + "roundUp": "If the result `share` should be rounded up.", + "token": "The ERC-20 token." + }, + "returns": { + "share": "The token amount represented in shares." + } + }, + "transfer(address,address,address,uint256)": { + "params": { + "from": "which user to pull the tokens.", + "share": "The amount of `token` in shares.", + "to": "which user to push the tokens.", + "token": "The ERC-20 token to transfer." + } + }, + "transferMultiple(address,address,address[],uint256[])": { + "params": { + "from": "which user to pull the tokens.", + "shares": "The amount of `token` in shares for each receiver in `tos`.", + "token": "The ERC-20 token to transfer.", + "tos": "The receivers of the tokens." + } + }, + "transferOwnership(address,bool,bool)": { + "params": { + "direct": "True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.", + "newOwner": "Address of the new owner.", + "renounce": "Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise." + } + }, + "withdraw(address,address,address,uint256,uint256)": { + "params": { + "amount": "of tokens. Either one of `amount` or `share` needs to be supplied.", + "from": "which user to pull the tokens.", + "share": "Like above, but `share` takes precedence over `amount`.", + "to": "which user to push the tokens.", + "token_": "The ERC-20 token to withdraw." + } + } + }, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "batch(bytes[],bool)": { + "notice": "Allows batched call to self (this contract)." + }, + "batchFlashLoan(address,address[],address[],uint256[],bytes)": { + "notice": "Support for batched flashloans. Useful to request multiple different `tokens` in a single transaction." + }, + "claimOwnership()": { + "notice": "Needs to be called by `pendingOwner` to claim ownership." + }, + "deploy(address,bytes,bool)": { + "notice": "Deploys a given master Contract as a clone. Any ETH transferred with this call is forwarded to the new clone. Emits `LogDeploy`." + }, + "deposit(address,address,address,uint256,uint256)": { + "notice": "Deposit an amount of `token` represented in either `amount` or `share`." + }, + "flashLoan(address,address,address,uint256,bytes)": { + "notice": "Flashloan ability." + }, + "harvest(address,bool,uint256)": { + "notice": "The actual process of yield farming. Executes the strategy of `token`. Optionally does housekeeping if `balance` is true. `maxChangeAmount` is relevant for skimming or withdrawing if `balance` is true." + }, + "masterContractApproved(address,address)": { + "notice": "masterContract to user to approval state" + }, + "masterContractOf(address)": { + "notice": "Mapping from clone contracts to their masterContract." + }, + "nonces(address)": { + "notice": "user nonces for masterContract approvals" + }, + "permitToken(address,address,address,uint256,uint256,uint8,bytes32,bytes32)": { + "notice": "Call wrapper that performs `ERC20.permit` on `token`. Lookup `IERC20.permit`." + }, + "registerProtocol()": { + "notice": "Other contracts need to register with this master contract so that users can approve them for the BentoBox." + }, + "setMasterContractApproval(address,address,bool,uint8,bytes32,bytes32)": { + "notice": "Approves or revokes a `masterContract` access to `user` funds." + }, + "setStrategy(address,address)": { + "notice": "Sets the contract address of a new strategy that conforms to `IStrategy` for `token`. Must be called twice with the same arguments. A new strategy becomes pending first and can be activated once `STRATEGY_DELAY` is over." + }, + "setStrategyTargetPercentage(address,uint64)": { + "notice": "Sets the target percentage of the strategy for `token`." + }, + "transfer(address,address,address,uint256)": { + "notice": "Transfer shares from a user account to another one." + }, + "transferMultiple(address,address,address[],uint256[])": { + "notice": "Transfer shares from a user account to multiple other ones." + }, + "transferOwnership(address,bool,bool)": { + "notice": "Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`." + }, + "whitelistMasterContract(address,bool)": { + "notice": "Enables or disables a contract for approval without signed message." + }, + "whitelistedMasterContracts(address)": { + "notice": "masterContract to whitelisted state for approval without signed message" + }, + "withdraw(address,address,address,uint256,uint256)": { + "notice": "Withdraws an amount of `token` from a user account." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 900, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 902, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 1046, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "masterContractOf", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_address)" + }, + { + "astId": 1140, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "masterContractApproved", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))" + }, + { + "astId": 1145, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "whitelistedMasterContracts", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_bool)" + }, + { + "astId": 1150, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "nonces", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 1701, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "balanceOf", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_contract(IERC20)67,t_mapping(t_address,t_uint256))" + }, + { + "astId": 1705, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "totals", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_contract(IERC20)67,t_struct(Rebase)550_storage)" + }, + { + "astId": 1709, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "strategy", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_contract(IERC20)67,t_contract(IStrategy)142)" + }, + { + "astId": 1713, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "pendingStrategy", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_contract(IERC20)67,t_contract(IStrategy)142)" + }, + { + "astId": 1717, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "strategyData", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_contract(IERC20)67,t_struct(StrategyData)1673_storage)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "encoding": "inplace", + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IERC20)67": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IStrategy)142": { + "encoding": "inplace", + "label": "contract IStrategy", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_address)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => address)", + "numberOfBytes": "32", + "value": "t_address" + }, + "t_mapping(t_address,t_bool)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => bool)", + "numberOfBytes": "32", + "value": "t_bool" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_bool)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_contract(IERC20)67,t_contract(IStrategy)142)": { + "encoding": "mapping", + "key": "t_contract(IERC20)67", + "label": "mapping(contract IERC20 => contract IStrategy)", + "numberOfBytes": "32", + "value": "t_contract(IStrategy)142" + }, + "t_mapping(t_contract(IERC20)67,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_contract(IERC20)67", + "label": "mapping(contract IERC20 => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_contract(IERC20)67,t_struct(Rebase)550_storage)": { + "encoding": "mapping", + "key": "t_contract(IERC20)67", + "label": "mapping(contract IERC20 => struct Rebase)", + "numberOfBytes": "32", + "value": "t_struct(Rebase)550_storage" + }, + "t_mapping(t_contract(IERC20)67,t_struct(StrategyData)1673_storage)": { + "encoding": "mapping", + "key": "t_contract(IERC20)67", + "label": "mapping(contract IERC20 => struct BentoBoxV1.StrategyData)", + "numberOfBytes": "32", + "value": "t_struct(StrategyData)1673_storage" + }, + "t_struct(Rebase)550_storage": { + "encoding": "inplace", + "label": "struct Rebase", + "members": [ + { + "astId": 547, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "elastic", + "offset": 0, + "slot": "0", + "type": "t_uint128" + }, + { + "astId": 549, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "base", + "offset": 16, + "slot": "0", + "type": "t_uint128" + } + ], + "numberOfBytes": "32" + }, + "t_struct(StrategyData)1673_storage": { + "encoding": "inplace", + "label": "struct BentoBoxV1.StrategyData", + "members": [ + { + "astId": 1668, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "strategyStartDate", + "offset": 0, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 1670, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "targetPercentage", + "offset": 8, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 1672, + "contract": "contracts/mocks/BentoBoxMock.sol:BentoBoxMock", + "label": "balance", + "offset": 16, + "slot": "0", + "type": "t_uint128" + } + ], + "numberOfBytes": "32" + }, + "t_uint128": { + "encoding": "inplace", + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/GuineasMock.json b/deployments/ropsten/GuineasMock.json new file mode 100644 index 00000000..9f5d218c --- /dev/null +++ b/deployments/ropsten/GuineasMock.json @@ -0,0 +1,428 @@ +{ + "address": "0x0293DBe459c09f8360b5Edb2dc8f6E669d11D3B8", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_initialAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner_", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x9bcf15ba6b169c5445e8db90c08851b3992ce71cd9e72161b08f6251b4fe1ea1", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0x0293DBe459c09f8360b5Edb2dc8f6E669d11D3B8", + "transactionIndex": 36, + "gasUsed": "661626", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x5fe8b98b7df0e660789d749113a5b3ca28b35caa7d1f73f90fa5edfa582129e4", + "transactionHash": "0x9bcf15ba6b169c5445e8db90c08851b3992ce71cd9e72161b08f6251b4fe1ea1", + "logs": [], + "blockNumber": 12212441, + "cumulativeGasUsed": "2056579", + "status": 1, + "byzantium": true + }, + "args": [ + "340282366920938463463374607431768211455" + ], + "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_initialAmount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"approve(address,uint256)\":{\"params\":{\"amount\":\"The maximum collective amount that `spender` can draw.\",\"spender\":\"Address of the party that can draw from msg.sender's account.\"},\"returns\":{\"_0\":\"(bool) Returns True if approved.\"}},\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"deadline\":\"This permit must be redeemed before this deadline (UTC timestamp in seconds).\",\"owner_\":\"Address of the owner.\",\"spender\":\"The address of the spender that gets approved to draw from `owner_`.\",\"value\":\"The maximum collective amount that `spender` can draw.\"}},\"transfer(address,uint256)\":{\"params\":{\"amount\":\"of the tokens to move.\",\"to\":\"The address to move the tokens.\"},\"returns\":{\"_0\":\"(bool) Returns True if succeeded.\"}},\"transferFrom(address,address,uint256)\":{\"params\":{\"amount\":\"The token amount to move.\",\"from\":\"Address to draw tokens from.\",\"to\":\"The address to move the tokens.\"},\"returns\":{\"_0\":\"(bool) Returns True if succeeded.\"}}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"allowance(address,address)\":{\"notice\":\"owner > spender > allowance mapping.\"},\"approve(address,uint256)\":{\"notice\":\"Approves `amount` from sender to be spend by `spender`.\"},\"balanceOf(address)\":{\"notice\":\"owner > balance mapping.\"},\"nonces(address)\":{\"notice\":\"owner > nonce mapping. Used in `permit`.\"},\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Approves `value` from `owner_` to be spend by `spender`.\"},\"transfer(address,uint256)\":{\"notice\":\"Transfers `amount` tokens from `msg.sender` to `to`.\"},\"transferFrom(address,address,uint256)\":{\"notice\":\"Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/ERC20Mock.sol\":\"ERC20Mock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/ERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./interfaces/IERC20.sol\\\";\\nimport \\\"./Domain.sol\\\";\\n\\n// solhint-disable no-inline-assembly\\n// solhint-disable not-rely-on-time\\n\\n// Data part taken out for building of contracts that receive delegate calls\\ncontract ERC20Data {\\n /// @notice owner > balance mapping.\\n mapping(address => uint256) public balanceOf;\\n /// @notice owner > spender > allowance mapping.\\n mapping(address => mapping(address => uint256)) public allowance;\\n /// @notice owner > nonce mapping. Used in `permit`.\\n mapping(address => uint256) public nonces;\\n}\\n\\nabstract contract ERC20 is IERC20, Domain {\\n /// @notice owner > balance mapping.\\n mapping(address => uint256) public override balanceOf;\\n /// @notice owner > spender > allowance mapping.\\n mapping(address => mapping(address => uint256)) public override allowance;\\n /// @notice owner > nonce mapping. Used in `permit`.\\n mapping(address => uint256) public nonces;\\n\\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\\n\\n /// @notice Transfers `amount` tokens from `msg.sender` to `to`.\\n /// @param to The address to move the tokens.\\n /// @param amount of the tokens to move.\\n /// @return (bool) Returns True if succeeded.\\n function transfer(address to, uint256 amount) public returns (bool) {\\n // If `amount` is 0, or `msg.sender` is `to` nothing happens\\n if (amount != 0 || msg.sender == to) {\\n uint256 srcBalance = balanceOf[msg.sender];\\n require(srcBalance >= amount, \\\"ERC20: balance too low\\\");\\n if (msg.sender != to) {\\n require(to != address(0), \\\"ERC20: no zero address\\\"); // Moved down so low balance calls safe some gas\\n\\n balanceOf[msg.sender] = srcBalance - amount; // Underflow is checked\\n balanceOf[to] += amount;\\n }\\n }\\n emit Transfer(msg.sender, to, amount);\\n return true;\\n }\\n\\n /// @notice Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\\n /// @param from Address to draw tokens from.\\n /// @param to The address to move the tokens.\\n /// @param amount The token amount to move.\\n /// @return (bool) Returns True if succeeded.\\n function transferFrom(\\n address from,\\n address to,\\n uint256 amount\\n ) public returns (bool) {\\n // If `amount` is 0, or `from` is `to` nothing happens\\n if (amount != 0) {\\n uint256 srcBalance = balanceOf[from];\\n require(srcBalance >= amount, \\\"ERC20: balance too low\\\");\\n\\n if (from != to) {\\n uint256 spenderAllowance = allowance[from][msg.sender];\\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\\n if (spenderAllowance != type(uint256).max) {\\n require(spenderAllowance >= amount, \\\"ERC20: allowance too low\\\");\\n allowance[from][msg.sender] = spenderAllowance - amount; // Underflow is checked\\n }\\n require(to != address(0), \\\"ERC20: no zero address\\\"); // Moved down so other failed calls safe some gas\\n\\n balanceOf[from] = srcBalance - amount; // Underflow is checked\\n balanceOf[to] += amount;\\n }\\n }\\n emit Transfer(from, to, amount);\\n return true;\\n }\\n\\n /// @notice Approves `amount` from sender to be spend by `spender`.\\n /// @param spender Address of the party that can draw from msg.sender's account.\\n /// @param amount The maximum collective amount that `spender` can draw.\\n /// @return (bool) Returns True if approved.\\n function approve(address spender, uint256 amount) public override returns (bool) {\\n allowance[msg.sender][spender] = amount;\\n emit Approval(msg.sender, spender, amount);\\n return true;\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // keccak256(\\\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\\\");\\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\\n\\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\\n /// @param owner_ Address of the owner.\\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\\n /// @param value The maximum collective amount that `spender` can draw.\\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\\n function permit(\\n address owner_,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external override {\\n require(owner_ != address(0), \\\"ERC20: Owner cannot be 0\\\");\\n require(block.timestamp < deadline, \\\"ERC20: Expired\\\");\\n require(\\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\\n owner_,\\n \\\"ERC20: Invalid Signature\\\"\\n );\\n allowance[owner_][spender] = value;\\n emit Approval(owner_, spender, value);\\n }\\n}\\n\\ncontract ERC20WithSupply is IERC20, ERC20 {\\n uint256 public override totalSupply;\\n\\n function _mint(address user, uint256 amount) internal {\\n uint256 newTotalSupply = totalSupply + amount;\\n require(newTotalSupply >= totalSupply, \\\"Mint overflow\\\");\\n totalSupply = newTotalSupply;\\n balanceOf[user] += amount;\\n emit Transfer(address(0), user, amount);\\n }\\n\\n function _burn(address user, uint256 amount) internal {\\n require(balanceOf[user] >= amount, \\\"Burn too much\\\");\\n totalSupply -= amount;\\n balanceOf[user] -= amount;\\n emit Transfer(user, address(0), amount);\\n }\\n}\\n\",\"keccak256\":\"0xda643d3376730a9b125c3daadc31f7cd1f867a1ed5aa7722948da429832ea85c\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"contracts/mocks/ERC20Mock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"@boringcrypto/boring-solidity/contracts/ERC20.sol\\\";\\n\\ncontract ERC20Mock is ERC20 {\\n uint256 public override totalSupply;\\n\\n constructor(uint256 _initialAmount) public {\\n // Give the creator all initial tokens\\n balanceOf[msg.sender] = _initialAmount;\\n // Update total supply\\n totalSupply = _initialAmount;\\n }\\n}\\n\",\"keccak256\":\"0xb424cd1870af45710e9d0fdd60bbe04c08138d6727500f0ad30ab37d4dd7989f\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60c060405234801561001057600080fd5b50604051610b05380380610b058339818101604052602081101561003357600080fd5b50514660a081905261004481610062565b608052503360009081526020819052604090208190556003556100bb565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692186020808301919091528183019390935230606080830191909152825180830390910181526080909101909152805191012090565b60805160a051610a276100de600039806108a15250806108d65250610a276000f3fe608060405234801561001057600080fd5b50600436106100935760003560e01c806370a082311161006657806370a08231146101305780637ecebe0014610156578063a9059cbb1461017c578063d505accf146101a8578063dd62ed3e146101fb57610093565b8063095ea7b31461009857806318160ddd146100d857806323b872dd146100f25780633644e51514610128575b600080fd5b6100c4600480360360408110156100ae57600080fd5b506001600160a01b038135169060200135610229565b604080519115158252519081900360200190f35b6100e061028f565b60408051918252519081900360200190f35b6100c46004803603606081101561010857600080fd5b506001600160a01b03813581169160208101359091169060400135610295565b6100e06104a0565b6100e06004803603602081101561014657600080fd5b50356001600160a01b03166104af565b6100e06004803603602081101561016c57600080fd5b50356001600160a01b03166104c1565b6100c46004803603604081101561019257600080fd5b506001600160a01b0381351690602001356104d3565b6101f9600480360360e08110156101be57600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135610627565b005b6100e06004803603604081101561021157600080fd5b506001600160a01b038135811691602001351661087f565b3360008181526001602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b60035481565b6000811561044b576001600160a01b03841660009081526020819052604090205482811015610304576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b836001600160a01b0316856001600160a01b031614610449576001600160a01b038516600090815260016020908152604080832033845290915290205460001981146103c8578381101561039f576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20616c6c6f77616e636520746f6f206c6f770000000000000000604482015290519081900360640190fd5b6001600160a01b0386166000908152600160209081526040808320338452909152902084820390555b6001600160a01b03851661041c576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b506001600160a01b0380861660009081526020819052604080822086850390559186168152208054840190555b505b826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a35060019392505050565b60006104aa61089c565b905090565b60006020819052908152604090205481565b60026020526000908152604090205481565b6000811515806104eb5750336001600160a01b038416145b156105de57336000908152602081905260409020548281101561054e576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b336001600160a01b038516146105dc576001600160a01b0384166105b2576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b3360009081526020819052604080822085840390556001600160a01b038616825290208054840190555b505b6040805183815290516001600160a01b0385169133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a350600192915050565b6001600160a01b038716610682576040805162461bcd60e51b815260206004820152601860248201527f45524332303a204f776e65722063616e6e6f7420626520300000000000000000604482015290519081900360640190fd5b8342106106c7576040805162461bcd60e51b815260206004820152600e60248201526d115490cc8c0e88115e1c1a5c995960921b604482015290519081900360640190fd5b6001600160a01b038088166000818152600260209081526040918290208054600181810190925583517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981850152808501869052958c166060870152608086018b905260a086015260c08086018a90528351808703909101815260e0909501909252835193019290922090919061075d906108fc565b85858560405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156107b4573d6000803e3d6000fd5b505050602060405103516001600160a01b031614610819576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20496e76616c6964205369676e61747572650000000000000000604482015290519081900360640190fd5b6001600160a01b038088166000818152600160209081526040808320948b1680845294825291829020899055815189815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050505050565b600160209081526000928352604080842090915290825290205481565b6000467f000000000000000000000000000000000000000000000000000000000000000081146108d4576108cf81610998565b6108f6565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b600060405180604001604052806002815260200161190160f01b81525061092161089c565b836040516020018084805190602001908083835b602083106109545780518252601f199092019160209182019101610935565b51815160209384036101000a600019018019909216911617905292019485525083810192909252506040805180840383018152928101905281519101209392505050565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218602080830191909152818301939093523060608083019190915282518083039091018152608090910190915280519101209056fea2646970667358221220b002009c59b5c02907d6ed7f420b57304fbe13b1de173895e3686190753207c964736f6c634300060c0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100935760003560e01c806370a082311161006657806370a08231146101305780637ecebe0014610156578063a9059cbb1461017c578063d505accf146101a8578063dd62ed3e146101fb57610093565b8063095ea7b31461009857806318160ddd146100d857806323b872dd146100f25780633644e51514610128575b600080fd5b6100c4600480360360408110156100ae57600080fd5b506001600160a01b038135169060200135610229565b604080519115158252519081900360200190f35b6100e061028f565b60408051918252519081900360200190f35b6100c46004803603606081101561010857600080fd5b506001600160a01b03813581169160208101359091169060400135610295565b6100e06104a0565b6100e06004803603602081101561014657600080fd5b50356001600160a01b03166104af565b6100e06004803603602081101561016c57600080fd5b50356001600160a01b03166104c1565b6100c46004803603604081101561019257600080fd5b506001600160a01b0381351690602001356104d3565b6101f9600480360360e08110156101be57600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135610627565b005b6100e06004803603604081101561021157600080fd5b506001600160a01b038135811691602001351661087f565b3360008181526001602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b60035481565b6000811561044b576001600160a01b03841660009081526020819052604090205482811015610304576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b836001600160a01b0316856001600160a01b031614610449576001600160a01b038516600090815260016020908152604080832033845290915290205460001981146103c8578381101561039f576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20616c6c6f77616e636520746f6f206c6f770000000000000000604482015290519081900360640190fd5b6001600160a01b0386166000908152600160209081526040808320338452909152902084820390555b6001600160a01b03851661041c576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b506001600160a01b0380861660009081526020819052604080822086850390559186168152208054840190555b505b826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a35060019392505050565b60006104aa61089c565b905090565b60006020819052908152604090205481565b60026020526000908152604090205481565b6000811515806104eb5750336001600160a01b038416145b156105de57336000908152602081905260409020548281101561054e576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b336001600160a01b038516146105dc576001600160a01b0384166105b2576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b3360009081526020819052604080822085840390556001600160a01b038616825290208054840190555b505b6040805183815290516001600160a01b0385169133917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a350600192915050565b6001600160a01b038716610682576040805162461bcd60e51b815260206004820152601860248201527f45524332303a204f776e65722063616e6e6f7420626520300000000000000000604482015290519081900360640190fd5b8342106106c7576040805162461bcd60e51b815260206004820152600e60248201526d115490cc8c0e88115e1c1a5c995960921b604482015290519081900360640190fd5b6001600160a01b038088166000818152600260209081526040918290208054600181810190925583517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981850152808501869052958c166060870152608086018b905260a086015260c08086018a90528351808703909101815260e0909501909252835193019290922090919061075d906108fc565b85858560405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156107b4573d6000803e3d6000fd5b505050602060405103516001600160a01b031614610819576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20496e76616c6964205369676e61747572650000000000000000604482015290519081900360640190fd5b6001600160a01b038088166000818152600160209081526040808320948b1680845294825291829020899055815189815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050505050565b600160209081526000928352604080842090915290825290205481565b6000467f000000000000000000000000000000000000000000000000000000000000000081146108d4576108cf81610998565b6108f6565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b600060405180604001604052806002815260200161190160f01b81525061092161089c565b836040516020018084805190602001908083835b602083106109545780518252601f199092019160209182019101610935565b51815160209384036101000a600019018019909216911617905292019485525083810192909252506040805180840383018152928101905281519101209392505050565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218602080830191909152818301939093523060608083019190915282518083039091018152608090910190915280519101209056fea2646970667358221220b002009c59b5c02907d6ed7f420b57304fbe13b1de173895e3686190753207c964736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": { + "approve(address,uint256)": { + "params": { + "amount": "The maximum collective amount that `spender` can draw.", + "spender": "Address of the party that can draw from msg.sender's account." + }, + "returns": { + "_0": "(bool) Returns True if approved." + } + }, + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)": { + "params": { + "deadline": "This permit must be redeemed before this deadline (UTC timestamp in seconds).", + "owner_": "Address of the owner.", + "spender": "The address of the spender that gets approved to draw from `owner_`.", + "value": "The maximum collective amount that `spender` can draw." + } + }, + "transfer(address,uint256)": { + "params": { + "amount": "of the tokens to move.", + "to": "The address to move the tokens." + }, + "returns": { + "_0": "(bool) Returns True if succeeded." + } + }, + "transferFrom(address,address,uint256)": { + "params": { + "amount": "The token amount to move.", + "from": "Address to draw tokens from.", + "to": "The address to move the tokens." + }, + "returns": { + "_0": "(bool) Returns True if succeeded." + } + } + }, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "allowance(address,address)": { + "notice": "owner > spender > allowance mapping." + }, + "approve(address,uint256)": { + "notice": "Approves `amount` from sender to be spend by `spender`." + }, + "balanceOf(address)": { + "notice": "owner > balance mapping." + }, + "nonces(address)": { + "notice": "owner > nonce mapping. Used in `permit`." + }, + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)": { + "notice": "Approves `value` from `owner_` to be spend by `spender`." + }, + "transfer(address,uint256)": { + "notice": "Transfers `amount` tokens from `msg.sender` to `to`." + }, + "transferFrom(address,address,uint256)": { + "notice": "Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 924, + "contract": "contracts/mocks/ERC20Mock.sol:ERC20Mock", + "label": "balanceOf", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 932, + "contract": "contracts/mocks/ERC20Mock.sol:ERC20Mock", + "label": "allowance", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))" + }, + { + "astId": 937, + "contract": "contracts/mocks/ERC20Mock.sol:ERC20Mock", + "label": "nonces", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 18291, + "contract": "contracts/mocks/ERC20Mock.sol:ERC20Mock", + "label": "totalSupply", + "offset": 0, + "slot": "3", + "type": "t_uint256" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/NFTPair.json b/deployments/ropsten/NFTPair.json new file mode 100644 index 00000000..e8d5a1a3 --- /dev/null +++ b/deployments/ropsten/NFTPair.json @@ -0,0 +1,1179 @@ +{ + "address": "0xC1E7A584E70D134e750A2B429E10329dda302B70", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IBentoBoxV1", + "name": "bentoBox_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newFeeTo", + "type": "address" + } + ], + "name": "LogFeeTo", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "LogLend", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "LogRemoveCollateral", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "LogRepay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "name": "LogRequestLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "name": "LogUpdateLoanParams", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "feeTo", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeShare", + "type": "uint256" + } + ], + "name": "LogWithdrawFees", + "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": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bentoBox", + "outputs": [ + { + "internalType": "contract IBentoBoxV1", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "t", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "aprBPS", + "type": "uint16" + } + ], + "name": "calculateInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "interest", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "claimOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collateral", + "outputs": [ + { + "internalType": "contract IERC721", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8[]", + "name": "actions", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "datas", + "type": "bytes[]" + } + ], + "name": "cook", + "outputs": [ + { + "internalType": "uint256", + "name": "value1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value2", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "feeTo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feesEarnedShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "init", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "accepted", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "lend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "masterContract", + "outputs": [ + { + "internalType": "contract NFTPair", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "removeCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "repay", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skimCollateral", + "type": "bool" + }, + { + "internalType": "bool", + "name": "anyTokenId", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "requestAndBorrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "requestLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newFeeTo", + "type": "address" + } + ], + "name": "setFeeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skimFunds", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "takeCollateralAndLend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokenLoan", + "outputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "internalType": "uint64", + "name": "startTime", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokenLoanParams", + "outputs": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + }, + { + "internalType": "bool", + "name": "direct", + "type": "bool" + }, + { + "internalType": "bool", + "name": "renounce", + "type": "bool" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + } + ], + "name": "updateLoanParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x1542f1855ef280bdb397517e235d862840b31c270d8e7f5d9de9c2b9ddc8b955", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0xC1E7A584E70D134e750A2B429E10329dda302B70", + "transactionIndex": 26, + "gasUsed": "3505038", + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000020000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000020000000000000000000800000000000002000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000800000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000010000000", + "blockHash": "0xda3e21e8e32647c79889db620312b1b173ff1edf2b654bd91d418da399952d39", + "transactionHash": "0x1542f1855ef280bdb397517e235d862840b31c270d8e7f5d9de9c2b9ddc8b955", + "logs": [ + { + "transactionIndex": 26, + "blockNumber": 12210501, + "transactionHash": "0x1542f1855ef280bdb397517e235d862840b31c270d8e7f5d9de9c2b9ddc8b955", + "address": "0xC1E7A584E70D134e750A2B429E10329dda302B70", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000063a1e3877b1662a9ad124f8611b06e3ffbc29cba" + ], + "data": "0x", + "logIndex": 12, + "blockHash": "0xda3e21e8e32647c79889db620312b1b173ff1edf2b654bd91d418da399952d39" + } + ], + "blockNumber": 12210501, + "cumulativeGasUsed": "6342695", + "status": 1, + "byzantium": true + }, + "args": [ + "0xf5bce5077908a1b7370b9ae04adc565ebd643966" + ], + "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"bentoBox_\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"LogFeeTo\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogLend\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"LogRemoveCollateral\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogRepay\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogRequestLoan\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogUpdateLoanParams\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"feeTo\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"LogWithdrawFees\",\"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\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"asset\",\"outputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bentoBox\",\"outputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"principal\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"t\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"aprBPS\",\"type\":\"uint16\"}],\"name\":\"calculateInterest\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"interest\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"claimOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"collateral\",\"outputs\":[{\"internalType\":\"contract IERC721\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8[]\",\"name\":\"actions\",\"type\":\"uint8[]\"},{\"internalType\":\"uint256[]\",\"name\":\"values\",\"type\":\"uint256[]\"},{\"internalType\":\"bytes[]\",\"name\":\"datas\",\"type\":\"bytes[]\"}],\"name\":\"cook\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"value1\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"value2\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeTo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feesEarnedShare\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"init\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"accepted\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"lend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"masterContract\",\"outputs\":[{\"internalType\":\"contract NFTPair\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"removeCollateral\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"repay\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimCollateral\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"anyTokenId\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"requestAndBorrow\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"requestLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"setFeeTo\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimFunds\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"takeCollateralAndLend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoan\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"startTime\",\"type\":\"uint64\"},{\"internalType\":\"uint8\",\"name\":\"status\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoanParams\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"direct\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"renounce\",\"type\":\"bool\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"name\":\"updateLoanParams\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.\",\"kind\":\"dev\",\"methods\":{\"cook(uint8[],uint256[],bytes[])\":{\"params\":{\"actions\":\"An array with a sequence of actions to execute (see ACTION_ declarations).\",\"datas\":\"A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\",\"values\":\"A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\"},\"returns\":{\"value1\":\"May contain the first positioned return value of the last executed action (if applicable).\",\"value2\":\"May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\"}},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"params\":{\"accepted\":\"Loan parameters as the lender saw them, for security\",\"skim\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"removeCollateral(uint256,address)\":{\"params\":{\"to\":\"The receiver of the token.\",\"tokenId\":\"The token\"}},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"anyTokenId\":\"Set if lender agreed to any token. Must have tokenId 0 in signature.\",\"lender\":\"Lender, whose BentoBox balance the funds will come from\",\"params\":\"Loan parameters requested, and signed by the lender\",\"recipient\":\"Address to receive the loan.\",\"skimCollateral\":\"True if the collateral has already been transfered\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"params\":{\"params\":\"Loan conditions on offer\",\"skim\":\"True if the token has already been transfered\",\"to\":\"Address to receive the loan, or option to withdraw collateral\",\"tokenId\":\"ID of the NFT\"}},\"setFeeTo(address)\":{\"params\":{\"newFeeTo\":\"The address of the receiver.\"}},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"borrower\":\"Address that provides collateral and receives the loan\",\"params\":\"Loan terms offered, and signed by the borrower\",\"skimFunds\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"transferOwnership(address,bool,bool)\":{\"params\":{\"direct\":\"True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\",\"newOwner\":\"Address of the new owner.\",\"renounce\":\"Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\"}}},\"title\":\"NFTPair\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"calculateInterest(uint256,uint64,uint16)\":{\"notice\":\"Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected.\"},\"claimOwnership()\":{\"notice\":\"Needs to be called by `pendingOwner` to claim ownership.\"},\"constructor\":\"The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.\",\"cook(uint8[],uint256[],bytes[])\":{\"notice\":\"Executes a set of actions and allows composability (contract calls) to other contracts.\"},\"init(bytes)\":{\"notice\":\"De facto constructor for clone contracts\"},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"notice\":\"Lends with the parameters specified by the borrower.\"},\"removeCollateral(uint256,address)\":{\"notice\":\"Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan.\"},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Caller provides collateral; loan can go to a different address.\"},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"notice\":\"Deposit an NFT as collateral and request a loan against it\"},\"setFeeTo(address)\":{\"notice\":\"Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function.\"},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party.\"},\"transferOwnership(address,bool,bool)\":{\"notice\":\"Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`.\"},\"withdrawFees()\":{\"notice\":\"Withdraws the fees accumulated.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/NFTPair.sol\":\"NFTPair\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\\n// Edited by BoringCrypto\\n\\ncontract BoringOwnableData {\\n address public owner;\\n address public pendingOwner;\\n}\\n\\ncontract BoringOwnable is BoringOwnableData {\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /// @notice `owner` defaults to msg.sender on construction.\\n constructor() public {\\n owner = msg.sender;\\n emit OwnershipTransferred(address(0), msg.sender);\\n }\\n\\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\\n /// Can only be invoked by the current `owner`.\\n /// @param newOwner Address of the new owner.\\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\\n function transferOwnership(\\n address newOwner,\\n bool direct,\\n bool renounce\\n ) public onlyOwner {\\n if (direct) {\\n // Checks\\n require(newOwner != address(0) || renounce, \\\"Ownable: zero address\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, newOwner);\\n owner = newOwner;\\n pendingOwner = address(0);\\n } else {\\n // Effects\\n pendingOwner = newOwner;\\n }\\n }\\n\\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\\n function claimOwnership() public {\\n address _pendingOwner = pendingOwner;\\n\\n // Checks\\n require(msg.sender == _pendingOwner, \\\"Ownable: caller != pending owner\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, _pendingOwner);\\n owner = _pendingOwner;\\n pendingOwner = address(0);\\n }\\n\\n /// @notice Only allows the `owner` to execute the function.\\n modifier onlyOwner() {\\n require(msg.sender == owner, \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n}\\n\",\"keccak256\":\"0xbde1619421fef865bf5f5f806e319900fb862e27f0aef6e0878e93f04f477601\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IMasterContract {\\n /// @notice Init function that gets called from `BoringFactory.deploy`.\\n /// Also kown as the constructor for cloned contracts.\\n /// Any ETH send to `BoringFactory.deploy` ends up here.\\n /// @param data Can be abi encoded arguments or anything else.\\n function init(bytes calldata data) external payable;\\n}\\n\",\"keccak256\":\"0xc8d7519d2bd26fc6d5125f8fc3fe2a6aada76f71f26b4712e0a4160f1cbdb2ba\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"../interfaces/IERC20.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nlibrary BoringERC20 {\\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\\n\\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\\n if (data.length >= 64) {\\n return abi.decode(data, (string));\\n } else if (data.length == 32) {\\n uint8 i = 0;\\n while (i < 32 && data[i] != 0) {\\n i++;\\n }\\n bytes memory bytesArray = new bytes(i);\\n for (i = 0; i < 32 && data[i] != 0; i++) {\\n bytesArray[i] = data[i];\\n }\\n return string(bytesArray);\\n } else {\\n return \\\"???\\\";\\n }\\n }\\n\\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token symbol.\\n function safeSymbol(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token name.\\n function safeName(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (uint8) Token decimals.\\n function safeDecimals(IERC20 token) internal view returns (uint8) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\\n }\\n\\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\\n /// @param token The address of the ERC-20 token.\\n /// @param to The address of the user to check.\\n /// @return amount The token amount.\\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\\n require(success && data.length >= 32, \\\"BoringERC20: BalanceOf failed\\\");\\n amount = abi.decode(data, (uint256));\\n }\\n\\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransfer(\\n IERC20 token,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: Transfer failed\\\");\\n }\\n\\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param from Transfer tokens from.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransferFrom(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: TransferFrom failed\\\");\\n }\\n}\\n\",\"keccak256\":\"0xc0b0529bf740b422941fc4899762ef3bde7d05a56b1cdb60b063c2aa63883d65\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./BoringMath.sol\\\";\\n\\nstruct Rebase {\\n uint128 elastic;\\n uint128 base;\\n}\\n\\n/// @notice A rebasing library using overflow-/underflow-safe math.\\nlibrary RebaseLibrary {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n\\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\\n function toBase(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (uint256 base) {\\n if (total.elastic == 0) {\\n base = elastic;\\n } else {\\n base = elastic.mul(total.base) / total.elastic;\\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\\n base = base.add(1);\\n }\\n }\\n }\\n\\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\\n function toElastic(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (uint256 elastic) {\\n if (total.base == 0) {\\n elastic = base;\\n } else {\\n elastic = base.mul(total.elastic) / total.base;\\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\\n elastic = elastic.add(1);\\n }\\n }\\n }\\n\\n /// @notice Add `elastic` to `total` and doubles `total.base`.\\n /// @return (Rebase) The new total.\\n /// @return base in relationship to `elastic`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 base) {\\n base = toBase(total, elastic, roundUp);\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return (total, base);\\n }\\n\\n /// @notice Sub `base` from `total` and update `total.elastic`.\\n /// @return (Rebase) The new total.\\n /// @return elastic in relationship to `base`.\\n function sub(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 elastic) {\\n elastic = toElastic(total, base, roundUp);\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return (total, elastic);\\n }\\n\\n /// @notice Add `elastic` and `base` to `total`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return total;\\n }\\n\\n /// @notice Subtract `elastic` and `base` to `total`.\\n function sub(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return total;\\n }\\n\\n /// @notice Add `elastic` to `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.add(elastic.to128());\\n }\\n\\n /// @notice Subtract `elastic` from `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\\n }\\n}\\n\",\"keccak256\":\"0xab228bfa8a3019a4f7effa8aeeb05de141d328703d8a2f7b87ca811d0ca33196\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IBatchFlashBorrower {\\n function onBatchFlashLoan(\\n address sender,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n uint256[] calldata fees,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x825a46e61443df6e1289b513da4386d0413d0b5311553f3e7e7e5c90412ddd5d\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\\nimport './IBatchFlashBorrower.sol';\\nimport './IFlashBorrower.sol';\\nimport './IStrategy.sol';\\n\\ninterface IBentoBoxV1 {\\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\\n event LogRegisterProtocol(address indexed protocol);\\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\\n event LogStrategyDivest(address indexed token, uint256 amount);\\n event LogStrategyInvest(address indexed token, uint256 amount);\\n event LogStrategyLoss(address indexed token, uint256 amount);\\n event LogStrategyProfit(address indexed token, uint256 amount);\\n event LogStrategyQueued(address indexed token, address indexed strategy);\\n event LogStrategySet(address indexed token, address indexed strategy);\\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n function balanceOf(IERC20, address) external view returns (uint256);\\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\\n function claimOwnership() external;\\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\\n function masterContractApproved(address, address) external view returns (bool);\\n function masterContractOf(address) external view returns (address);\\n function nonces(address) external view returns (uint256);\\n function owner() external view returns (address);\\n function pendingOwner() external view returns (address);\\n function pendingStrategy(IERC20) external view returns (IStrategy);\\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\\n function registerProtocol() external;\\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\\n function strategy(IERC20) external view returns (IStrategy);\\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\\n function totals(IERC20) external view returns (Rebase memory totals_);\\n function transfer(IERC20 token, address from, address to, uint256 share) external;\\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\\n function whitelistMasterContract(address masterContract, bool approved) external;\\n function whitelistedMasterContracts(address) external view returns (bool);\\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\\n}\",\"keccak256\":\"0x9c025e34e0ef0c1fc9372ada9afa61925341ee93de9b9a79e77de55d715b6fb6\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IFlashBorrower {\\n function onFlashLoan(\\n address sender,\\n IERC20 token,\\n uint256 amount,\\n uint256 fee,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x6e389a5acb7b3e7f337b7e28477e998228f05fc4c8ff877eab32d3e15037ccc2\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IStrategy {\\n // Send the assets to the Strategy and call skim to invest them\\n function skim(uint256 amount) external;\\n\\n // Harvest any profits made converted to the asset and pass them to the caller\\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\\n\\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\\n\\n // Withdraw all assets in the safest way possible. This shouldn't fail.\\n function exit(uint256 balance) external returns (int256 amountAdded);\\n}\",\"keccak256\":\"0x91c02244e1508cf8e4d6c45110c57142301c237e809dcad67b8022f83555ba13\",\"license\":\"MIT\"},\"contracts/NFTPair.sol\":{\"content\":\"// SPDX-License-Identifier: UNLICENSED\\n\\n// Private Pool (NFT collateral)\\n\\n// ( ( (\\n// )\\\\ ) ( )\\\\ )\\\\ ) (\\n// (((_) ( /( ))\\\\ ((_)(()/( )( ( (\\n// )\\\\___ )(_)) /((_) _ ((_))(()\\\\ )\\\\ )\\\\ )\\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\\n// | (__ / _` || || || |/ _` | | '_|/ _ \\\\| ' \\\\))\\n// \\\\___|\\\\__,_| \\\\_,_||_|\\\\__,_| |_| \\\\___/|_||_|\\n\\n// Copyright (c) 2021 BoringCrypto - All rights reserved\\n// Twitter: @Boring_Crypto\\n\\n// Special thanks to:\\n// @0xKeno - for all his invaluable contributions\\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\\n\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/Domain.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\\\";\\nimport \\\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\\\";\\nimport \\\"./interfaces/IERC721.sol\\\";\\n\\nstruct TokenLoanParams {\\n uint128 valuation; // How much will you get? OK to owe until expiration.\\n uint64 duration; // Length of loan in seconds\\n uint16 annualInterestBPS; // Variable cost of taking out the loan\\n}\\n\\ninterface ILendingClub {\\n // Per token settings.\\n function willLend(uint256 tokenId, TokenLoanParams memory params)\\n external\\n view\\n returns (bool);\\n\\n function lendingConditions(address nftPair, uint256 tokenId)\\n external\\n view\\n returns (TokenLoanParams memory);\\n}\\n\\ninterface INFTPair {\\n function collateral() external view returns (IERC721);\\n\\n function asset() external view returns (IERC20);\\n\\n function masterContract() external view returns (address);\\n\\n function bentoBox() external view returns (IBentoBoxV1);\\n\\n function removeCollateral(uint256 tokenId, address to) external;\\n}\\n\\n/// @title NFTPair\\n/// @dev This contract allows contract calls to any contract (except BentoBox)\\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n using RebaseLibrary for Rebase;\\n using BoringERC20 for IERC20;\\n\\n event LogRequestLoan(\\n address indexed borrower,\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n event LogUpdateLoanParams(\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n // This automatically clears the associated loan, if any\\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\\n // Details are in the loan request\\n event LogLend(address indexed lender, uint256 indexed tokenId);\\n event LogRepay(address indexed from, uint256 indexed tokenId);\\n event LogFeeTo(address indexed newFeeTo);\\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\\n\\n // Immutables (for MasterContract and all clones)\\n IBentoBoxV1 public immutable bentoBox;\\n NFTPair public immutable masterContract;\\n\\n // MasterContract variables\\n address public feeTo;\\n\\n // Per clone variables\\n // Clone init settings\\n IERC721 public collateral;\\n IERC20 public asset;\\n\\n // A note on terminology:\\n // \\\"Shares\\\" are BentoBox shares.\\n\\n // Track assets we own. Used to allow skimming the excesss.\\n uint256 public feesEarnedShare;\\n\\n // Per token settings.\\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\\n\\n uint8 private constant LOAN_INITIAL = 0;\\n uint8 private constant LOAN_REQUESTED = 1;\\n uint8 private constant LOAN_OUTSTANDING = 2;\\n struct TokenLoan {\\n address borrower;\\n address lender;\\n uint64 startTime;\\n uint8 status;\\n }\\n mapping(uint256 => TokenLoan) public tokenLoan;\\n\\n // Do not go over 100% on either of these..\\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\\n uint256 private constant OPEN_FEE_BPS = 100;\\n uint256 private constant BPS = 10_000;\\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\\n\\n // Highest order term in the Maclaurin series for exp used by\\n // `calculateIntest`.\\n // Intuitive interpretation: interest continuously accrues on the principal.\\n // That interest, in turn, earns \\\"second-order\\\" interest-on-interest, which\\n // itself earns \\\"third-order\\\" interest, etc. This constant determines how\\n // far we take this until we stop counting.\\n //\\n // The error, in terms of the interest rate, is at least\\n //\\n // ----- n ----- Infinity\\n // \\\\ x^k \\\\ x^k\\n // e^x - ) --- , which is ) --- ,\\n // / k! / k!\\n // ----- k = 1 k ----- k = n + 1\\n //\\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\\n // interest that is owed at rate r over time t. It makes no difference if\\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\\n // is the same. Why \\\"at least\\\"? There are also rounding errors. See\\n // `calculateInterest` for more detail.\\n // The factorial in the denominator \\\"wins\\\"; for all reasonable (and quite\\n // a few unreasonable) interest rates, the lower-order terms contribute the\\n // most to the total. The following table lists some of the calculated\\n // approximations for different values of n, along with the \\\"true\\\" result:\\n //\\n // Total: 10% 20% 50% 100% 200% 500% 1000%\\n // -----------------------------------------------------------------------\\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\\n //\\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\\n //\\n // For instance, calculating the compounding effects of 200% in \\\"total\\\"\\n // interest to the sixth order results in 635.6%, whereas the true result\\n // is 638.9%.\\n // At 500% that difference is a little more dramatic, but it is still in\\n // the same ballpark -- and of little practical consequence unless the\\n // collateral can be expected to go up more than 112 times in value.\\n // Still, for volatile tokens, or an asset that is somehow known to be very\\n // inflationary, use a different number.\\n // Zero (no interest at all) is ignored and treated as one (linear only).\\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\\n\\n // For signed lend / borrow requests:\\n mapping(address => uint256) public nonces;\\n\\n /// @notice The constructor is only used for the initial master contract.\\n /// @notice Subsequent clones are initialised via `init`.\\n constructor(IBentoBoxV1 bentoBox_) public {\\n bentoBox = bentoBox_;\\n masterContract = this;\\n }\\n\\n /// @notice De facto constructor for clone contracts\\n function init(bytes calldata data) public payable override {\\n require(\\n address(collateral) == address(0),\\n \\\"NFTPair: already initialized\\\"\\n );\\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\\n require(address(collateral) != address(0), \\\"NFTPair: bad pair\\\");\\n }\\n\\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params)\\n public\\n {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_OUTSTANDING) {\\n // The lender can change terms so long as the changes are strictly\\n // the same or better for the borrower:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\\n require(\\n params.duration >= cur.duration &&\\n params.valuation <= cur.valuation &&\\n params.annualInterestBPS <= cur.annualInterestBPS,\\n \\\"NFTPair: worse params\\\"\\n );\\n } else if (loan.status == LOAN_REQUESTED) {\\n // The borrower has already deposited the collateral and can\\n // change whatever they like\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else {\\n // The loan has not been taken out yet; the borrower needs to\\n // provide collateral.\\n revert(\\\"NFTPair: no collateral\\\");\\n }\\n tokenLoanParams[tokenId] = params;\\n emit LogUpdateLoanParams(\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n function _requestLoan(\\n address collateralProvider,\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) private {\\n // Edge case: valuation can be zero. That effectively gifts the NFT and\\n // is therefore a bad idea, but does not break the contract.\\n require(\\n tokenLoan[tokenId].status == LOAN_INITIAL,\\n \\\"NFTPair: loan exists\\\"\\n );\\n if (skim) {\\n require(\\n collateral.ownerOf(tokenId) == address(this),\\n \\\"NFTPair: skim failed\\\"\\n );\\n } else {\\n collateral.transferFrom(collateralProvider, address(this), tokenId);\\n }\\n TokenLoan memory loan;\\n loan.borrower = to;\\n loan.status = LOAN_REQUESTED;\\n tokenLoan[tokenId] = loan;\\n tokenLoanParams[tokenId] = params;\\n\\n emit LogRequestLoan(\\n to,\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n /// @notice Deposit an NFT as collateral and request a loan against it\\n /// @param tokenId ID of the NFT\\n /// @param to Address to receive the loan, or option to withdraw collateral\\n /// @param params Loan conditions on offer\\n /// @param skim True if the token has already been transfered\\n function requestLoan(\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) public {\\n _requestLoan(msg.sender, tokenId, params, to, skim);\\n }\\n\\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\\n /// @notice This destroys the loan.\\n /// @param tokenId The token\\n /// @param to The receiver of the token.\\n function removeCollateral(uint256 tokenId, address to) public {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_REQUESTED) {\\n // We are withdrawing collateral that is not in use:\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else if (loan.status == LOAN_OUTSTANDING) {\\n // We are seizing collateral as the lender. The loan has to be\\n // expired and not paid off:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <=\\n block.timestamp,\\n \\\"NFTPair: not expired\\\"\\n );\\n }\\n // If there somehow is collateral but no accompanying loan, then anyone\\n // can claim it by first requesting a loan with `skim` set to true, and\\n // then withdrawing. So we might as well allow it here..\\n delete tokenLoan[tokenId];\\n collateral.transferFrom(address(this), to, tokenId);\\n emit LogRemoveCollateral(tokenId, to);\\n }\\n\\n // Assumes the lender has agreed to the loan.\\n function _lend(\\n address lender,\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) internal {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_REQUESTED, \\\"NFTPair: not available\\\");\\n TokenLoanParams memory params = tokenLoanParams[tokenId];\\n\\n // Valuation has to be an exact match, everything else must be at least\\n // as good for the lender as `accepted`.\\n require(\\n params.valuation == accepted.valuation &&\\n params.duration <= accepted.duration &&\\n params.annualInterestBPS >= accepted.annualInterestBPS,\\n \\\"NFTPair: bad params\\\"\\n );\\n\\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\\n\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare -\\n openFeeShare +\\n protocolFeeShare +\\n feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n } else {\\n bentoBox.transfer(\\n asset,\\n lender,\\n address(this),\\n totalShare - openFeeShare + protocolFeeShare\\n );\\n }\\n // No underflow: follows from OPEN_FEE_BPS <= BPS\\n uint256 borrowerShare = totalShare - openFeeShare;\\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\\n // No overflow: addends (and result) must fit in BentoBox\\n feesEarnedShare += protocolFeeShare;\\n\\n loan.lender = lender;\\n loan.status = LOAN_OUTSTANDING;\\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\\n tokenLoan[tokenId] = loan;\\n\\n emit LogLend(lender, tokenId);\\n }\\n\\n /// @notice Lends with the parameters specified by the borrower.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param accepted Loan parameters as the lender saw them, for security\\n /// @param skim True if the funds have been transfered to the contract\\n function lend(\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) public {\\n _lend(msg.sender, tokenId, accepted, skim);\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // NOTE on signature hashes: the domain separator only guarantees that the\\n // chain ID and master contract are a match, so we explicitly include the\\n // clone address (and the asset/collateral addresses):\\n\\n // keccak256(\\\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant LEND_SIGNATURE_HASH =\\n 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\\n\\n // keccak256(\\\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant BORROW_SIGNATURE_HASH =\\n 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\\n\\n /// @notice Request and immediately borrow from a pre-committed lender\\n\\n /// @notice Caller provides collateral; loan can go to a different address.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param lender Lender, whose BentoBox balance the funds will come from\\n /// @param recipient Address to receive the loan.\\n /// @param params Loan parameters requested, and signed by the lender\\n /// @param skimCollateral True if the collateral has already been transfered\\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\\n function requestAndBorrow(\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\\n require(\\n ILendingClub(lender).willLend(tokenId, params),\\n \\\"NFTPair: LendingClub does not like you\\\"\\n );\\n } else {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[lender]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n LEND_SIGNATURE_HASH,\\n address(this),\\n anyTokenId ? 0 : tokenId,\\n anyTokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == lender,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n }\\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\\n _lend(lender, tokenId, params, false);\\n }\\n\\n /// @notice Take collateral from a pre-commited borrower and lend against it\\n /// @notice Collateral must come from the borrower, not a third party.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param borrower Address that provides collateral and receives the loan\\n /// @param params Loan terms offered, and signed by the borrower\\n /// @param skimFunds True if the funds have been transfered to the contract\\n function takeCollateralAndLend(\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[borrower]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n BORROW_SIGNATURE_HASH,\\n address(this),\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == borrower,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n _requestLoan(borrower, tokenId, params, borrower, false);\\n _lend(msg.sender, tokenId, params, skimFunds);\\n }\\n\\n /// Approximates continuous compounding. Uses Horner's method to evaluate\\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\\n /// errors along the way. The following is always guaranteed:\\n ///\\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\\n ///\\n /// where time = t/YEAR, up to at most the rounding error obtained in\\n /// calculating linear interest.\\n ///\\n /// If the theoretical result that we are approximating (the rightmost part\\n /// of the above inquality) fits in 128 bits, then the function is\\n /// guaranteed not to revert (unless n > 250, which is way too high).\\n /// If even the linear interest (leftmost part of the inequality) does not\\n /// the function will revert.\\n /// Otherwise, the function may revert, return a reasonable result, or\\n /// return a very inaccurate result. Even then the above inequality is\\n /// respected.\\n function calculateInterest(\\n uint256 principal,\\n uint64 t,\\n uint16 aprBPS\\n ) public pure returns (uint256 interest) {\\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\\n //\\n // We calculate\\n //\\n // ----- n ----- n\\n // \\\\ principal * (t * aprBPS)^k \\\\\\n // ) -------------------------- =: ) term_k\\n // / k! * YEAR_BPS^k /\\n // ----- k = 1 ----- k = 1\\n //\\n // which approaches, but never exceeds the \\\"theoretical\\\" result,\\n //\\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\\n //\\n // as n goes to infinity. We use the fact that\\n //\\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\\n // term_k = ---------------------------------------------\\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\\n //\\n // t * aprBPS\\n // = term_{k-1} * ------------ (*)\\n // k * YEAR_BPS\\n //\\n // to calculate the terms one by one. The principal affords us the\\n // precision to carry out the division without resorting to fixed-point\\n // math. Any rounding error is downward, which we consider acceptable.\\n //\\n // Since all numbers involved are positive, each term is certainly\\n // bounded above by M. From (*) we see that any intermediate results\\n // are at most\\n //\\n // denom_k := k * YEAR_BPS.\\n //\\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\\n // which proves that all calculations will certainly not overflow if M\\n // fits in 128 bits.\\n //\\n // If M does not fit, then the intermediate results for some term may\\n // eventually overflow, but this cannot happen at the first term, and\\n // neither can the total overflow because it uses checked math.\\n //\\n // This constitutes a guarantee of specified behavior when M >= 2^128.\\n uint256 x = uint256(t) * aprBPS;\\n uint256 term_k = (principal * x) / YEAR_BPS;\\n uint256 denom_k = YEAR_BPS;\\n\\n interest = term_k;\\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\\n denom_k += YEAR_BPS;\\n term_k = (term_k * x) / denom_k;\\n interest = interest.add(term_k); // <- Only overflow check we need\\n }\\n\\n if (interest >= 2**128) {\\n revert();\\n }\\n }\\n\\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_OUTSTANDING, \\\"NFTPair: no loan\\\");\\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\\n \\\"NFTPair: loan expired\\\"\\n );\\n\\n uint128 principal = loanParams.valuation;\\n\\n // No underflow: loan.startTime is only ever set to a block timestamp\\n // Cast is safe: if this overflows, then all loans have expired anyway\\n uint256 interest = calculateInterest(\\n principal,\\n uint64(block.timestamp - loan.startTime),\\n loanParams.annualInterestBPS\\n ).to128();\\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\\n amount = principal + interest;\\n\\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\\n\\n address from;\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare + feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n from = address(this);\\n // No overflow: result fits in BentoBox\\n } else {\\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\\n from = msg.sender;\\n }\\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\\n feesEarnedShare += feeShare;\\n delete tokenLoan[tokenId];\\n\\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\\n collateral.transferFrom(address(this), loan.borrower, tokenId);\\n\\n emit LogRepay(from, tokenId);\\n }\\n\\n uint8 internal constant ACTION_REPAY = 2;\\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\\n\\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\\n uint8 internal constant ACTION_LEND = 13;\\n\\n // Function on BentoBox\\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\\n\\n // Any external call (except to BentoBox)\\n uint8 internal constant ACTION_CALL = 30;\\n\\n // Signed requests\\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\\n\\n int256 internal constant USE_VALUE1 = -1;\\n int256 internal constant USE_VALUE2 = -2;\\n\\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\\n function _num(\\n int256 inNum,\\n uint256 value1,\\n uint256 value2\\n ) internal pure returns (uint256 outNum) {\\n outNum = inNum >= 0\\n ? uint256(inNum)\\n : (inNum == USE_VALUE1 ? value1 : value2);\\n }\\n\\n /// @dev Helper function for depositing into `bentoBox`.\\n function _bentoDeposit(\\n bytes memory data,\\n uint256 value,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\\n share = int256(_num(share, value1, value2));\\n return\\n bentoBox.deposit{value: value}(\\n token,\\n msg.sender,\\n to,\\n uint256(amount),\\n uint256(share)\\n );\\n }\\n\\n /// @dev Helper function to withdraw from the `bentoBox`.\\n function _bentoWithdraw(\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n return\\n bentoBox.withdraw(\\n token,\\n msg.sender,\\n to,\\n _num(amount, value1, value2),\\n _num(share, value1, value2)\\n );\\n }\\n\\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\\n /// This also means that calls made from this contract shall *not* be trusted.\\n function _call(\\n uint256 value,\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (bytes memory, uint8) {\\n (\\n address callee,\\n bytes memory callData,\\n bool useValue1,\\n bool useValue2,\\n uint8 returnValues\\n ) = abi.decode(data, (address, bytes, bool, bool, uint8));\\n\\n if (useValue1 && !useValue2) {\\n callData = abi.encodePacked(callData, value1);\\n } else if (!useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value2);\\n } else if (useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value1, value2);\\n }\\n\\n require(\\n callee != address(bentoBox) &&\\n callee != address(collateral) &&\\n callee != address(this),\\n \\\"NFTPair: can't call\\\"\\n );\\n\\n (bool success, bytes memory returnData) = callee.call{value: value}(\\n callData\\n );\\n require(success, \\\"NFTPair: call failed\\\");\\n return (returnData, returnValues);\\n }\\n\\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\\n function cook(\\n uint8[] calldata actions,\\n uint256[] calldata values,\\n bytes[] calldata datas\\n ) external payable returns (uint256 value1, uint256 value2) {\\n for (uint256 i = 0; i < actions.length; i++) {\\n uint8 action = actions[i];\\n if (action == ACTION_REPAY) {\\n (uint256 tokenId, bool skim) = abi.decode(\\n datas[i],\\n (uint256, bool)\\n );\\n repay(tokenId, skim);\\n } else if (action == ACTION_REMOVE_COLLATERAL) {\\n (uint256 tokenId, address to) = abi.decode(\\n datas[i],\\n (uint256, address)\\n );\\n removeCollateral(tokenId, to);\\n } else if (action == ACTION_REQUEST_LOAN) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) = abi.decode(\\n datas[i],\\n (uint256, TokenLoanParams, address, bool)\\n );\\n requestLoan(tokenId, params, to, skim);\\n } else if (action == ACTION_LEND) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n bool skim\\n ) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\\n lend(tokenId, params, skim);\\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\\n (\\n address user,\\n address _masterContract,\\n bool approved,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (address, address, bool, uint8, bytes32, bytes32)\\n );\\n bentoBox.setMasterContractApproval(\\n user,\\n _masterContract,\\n approved,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_BENTO_DEPOSIT) {\\n (value1, value2) = _bentoDeposit(\\n datas[i],\\n values[i],\\n value1,\\n value2\\n );\\n } else if (action == ACTION_BENTO_WITHDRAW) {\\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\\n } else if (action == ACTION_BENTO_TRANSFER) {\\n (IERC20 token, address to, int256 share) = abi.decode(\\n datas[i],\\n (IERC20, address, int256)\\n );\\n bentoBox.transfer(\\n token,\\n msg.sender,\\n to,\\n _num(share, value1, value2)\\n );\\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\\n (\\n IERC20 token,\\n address[] memory tos,\\n uint256[] memory shares\\n ) = abi.decode(datas[i], (IERC20, address[], uint256[]));\\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\\n } else if (action == ACTION_CALL) {\\n (bytes memory returnData, uint8 returnValues) = _call(\\n values[i],\\n datas[i],\\n value1,\\n value2\\n );\\n\\n if (returnValues == 1) {\\n (value1) = abi.decode(returnData, (uint256));\\n } else if (returnValues == 2) {\\n (value1, value2) = abi.decode(\\n returnData,\\n (uint256, uint256)\\n );\\n }\\n } else if (action == ACTION_REQUEST_AND_BORROW) {\\n (\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n address,\\n TokenLoanParams,\\n bool,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n requestAndBorrow(\\n tokenId,\\n lender,\\n recipient,\\n params,\\n skimCollateral,\\n anyTokenId,\\n deadline,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\\n (\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n TokenLoanParams,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n takeCollateralAndLend(\\n tokenId,\\n borrower,\\n params,\\n skimFunds,\\n deadline,\\n v,\\n r,\\n s\\n );\\n }\\n }\\n }\\n\\n /// @notice Withdraws the fees accumulated.\\n function withdrawFees() public {\\n address to = masterContract.feeTo();\\n\\n uint256 _share = feesEarnedShare;\\n if (_share > 0) {\\n bentoBox.transfer(asset, address(this), to, _share);\\n feesEarnedShare = 0;\\n }\\n\\n emit LogWithdrawFees(to, _share);\\n }\\n\\n /// @notice Sets the beneficiary of fees accrued in liquidations.\\n /// MasterContract Only Admin function.\\n /// @param newFeeTo The address of the receiver.\\n function setFeeTo(address newFeeTo) public onlyOwner {\\n feeTo = newFeeTo;\\n emit LogFeeTo(newFeeTo);\\n }\\n}\\n\",\"keccak256\":\"0x2f116b73330c8b296bd38f699afc82f62753a7ced6b6a85a3f4e0cc717c4351c\",\"license\":\"UNLICENSED\"},\"contracts/interfaces/IERC165.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.0 <0.8.0;\\n\\n/**\\n * @dev Interface of the ERC165 standard, as defined in the\\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\\n *\\n * Implementers can declare support of contract interfaces, which can then be\\n * queried by others ({ERC165Checker}).\\n *\\n * For an implementation, see {ERC165}.\\n */\\ninterface IERC165 {\\n /**\\n * @dev Returns true if this contract implements the interface defined by\\n * `interfaceId`. See the corresponding\\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\\n * to learn more about how these ids are created.\\n *\\n * This function call must use less than 30 000 gas.\\n */\\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\\n}\\n\",\"keccak256\":\"0x568574015c35b45a03f3bc3857240fb9985380d3faa3df7207123620d48ffe13\",\"license\":\"MIT\"},\"contracts/interfaces/IERC721.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.2 <0.8.0;\\n\\nimport \\\"./IERC165.sol\\\";\\n\\n/**\\n * @dev Required interface of an ERC721 compliant contract.\\n */\\ninterface IERC721 is IERC165 {\\n /**\\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\\n */\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\\n */\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\\n */\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n /**\\n * @dev Returns the number of tokens in ``owner``'s account.\\n */\\n function balanceOf(address owner) external view returns (uint256 balance);\\n\\n /**\\n * @dev Returns the owner of the `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function ownerOf(uint256 tokenId) external view returns (address owner);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Transfers `tokenId` token from `from` to `to`.\\n *\\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\\n * The approval is cleared when the token is transferred.\\n *\\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\\n *\\n * Requirements:\\n *\\n * - The caller must own the token or be an approved operator.\\n * - `tokenId` must exist.\\n *\\n * Emits an {Approval} event.\\n */\\n function approve(address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Returns the account approved for `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function getApproved(uint256 tokenId) external view returns (address operator);\\n\\n /**\\n * @dev Approve or remove `operator` as an operator for the caller.\\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\\n *\\n * Requirements:\\n *\\n * - The `operator` cannot be the caller.\\n *\\n * Emits an {ApprovalForAll} event.\\n */\\n function setApprovalForAll(address operator, bool _approved) external;\\n\\n /**\\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\\n *\\n * See {setApprovalForAll}\\n */\\n function isApprovedForAll(address owner, address operator) external view returns (bool);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\\n}\\n\",\"keccak256\":\"0x0bfe878a4a6ddcdba3d5b53a21e76bcb84bc77a114fbd432a5533bda12f155fa\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x6101006040523480156200001257600080fd5b5060405162003fdf38038062003fdf8339810160408190526200003591620000fd565b600080546001600160a01b0319163390811782556040519091907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a34660a08190526200008581620000a7565b608052506001600160601b0319606091821b1660c05230901b60e0526200014c565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692188230604051602001620000e0939291906200012d565b604051602081830303815290604052805190602001209050919050565b6000602082840312156200010f578081fd5b81516001600160a01b038116811462000126578182fd5b9392505050565b92835260208301919091526001600160a01b0316604082015260600190565b60805160a05160c05160601c60e05160601c613e02620001dd600039806108ba5280611b825250806109705280610d115280610ed95280610fb152806111d7528061160352806116af5280611764528061182b52806118f35280611f115280611fed528061219c52806126fc52806127c5528061288a528061291d525080611e75525080611eaa5250613e026000f3fe6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", + "devdoc": { + "details": "This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.", + "kind": "dev", + "methods": { + "cook(uint8[],uint256[],bytes[])": { + "params": { + "actions": "An array with a sequence of actions to execute (see ACTION_ declarations).", + "datas": "A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.", + "values": "A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`." + }, + "returns": { + "value1": "May contain the first positioned return value of the last executed action (if applicable).", + "value2": "May contain the second positioned return value of the last executed action which returns 2 values (if applicable)." + } + }, + "lend(uint256,(uint128,uint64,uint16),bool)": { + "params": { + "accepted": "Loan parameters as the lender saw them, for security", + "skim": "True if the funds have been transfered to the contract", + "tokenId": "ID of the token that will function as collateral" + } + }, + "removeCollateral(uint256,address)": { + "params": { + "to": "The receiver of the token.", + "tokenId": "The token" + } + }, + "requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)": { + "params": { + "anyTokenId": "Set if lender agreed to any token. Must have tokenId 0 in signature.", + "lender": "Lender, whose BentoBox balance the funds will come from", + "params": "Loan parameters requested, and signed by the lender", + "recipient": "Address to receive the loan.", + "skimCollateral": "True if the collateral has already been transfered", + "tokenId": "ID of the token that will function as collateral" + } + }, + "requestLoan(uint256,(uint128,uint64,uint16),address,bool)": { + "params": { + "params": "Loan conditions on offer", + "skim": "True if the token has already been transfered", + "to": "Address to receive the loan, or option to withdraw collateral", + "tokenId": "ID of the NFT" + } + }, + "setFeeTo(address)": { + "params": { + "newFeeTo": "The address of the receiver." + } + }, + "takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)": { + "params": { + "borrower": "Address that provides collateral and receives the loan", + "params": "Loan terms offered, and signed by the borrower", + "skimFunds": "True if the funds have been transfered to the contract", + "tokenId": "ID of the token that will function as collateral" + } + }, + "transferOwnership(address,bool,bool)": { + "params": { + "direct": "True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.", + "newOwner": "Address of the new owner.", + "renounce": "Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise." + } + } + }, + "title": "NFTPair", + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "calculateInterest(uint256,uint64,uint16)": { + "notice": "Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected." + }, + "claimOwnership()": { + "notice": "Needs to be called by `pendingOwner` to claim ownership." + }, + "constructor": "The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.", + "cook(uint8[],uint256[],bytes[])": { + "notice": "Executes a set of actions and allows composability (contract calls) to other contracts." + }, + "init(bytes)": { + "notice": "De facto constructor for clone contracts" + }, + "lend(uint256,(uint128,uint64,uint16),bool)": { + "notice": "Lends with the parameters specified by the borrower." + }, + "removeCollateral(uint256,address)": { + "notice": "Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan." + }, + "requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)": { + "notice": "Caller provides collateral; loan can go to a different address." + }, + "requestLoan(uint256,(uint128,uint64,uint16),address,bool)": { + "notice": "Deposit an NFT as collateral and request a loan against it" + }, + "setFeeTo(address)": { + "notice": "Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function." + }, + "takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)": { + "notice": "Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party." + }, + "transferOwnership(address,bool,bool)": { + "notice": "Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`." + }, + "withdrawFees()": { + "notice": "Withdraws the fees accumulated." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 674, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 676, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 12042, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "feeTo", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 12044, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "collateral", + "offset": 0, + "slot": "3", + "type": "t_contract(IERC721)17337" + }, + { + "astId": 12046, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "asset", + "offset": 0, + "slot": "4", + "type": "t_contract(IERC20)1405" + }, + { + "astId": 12048, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "feesEarnedShare", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 12052, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "tokenLoanParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)" + }, + { + "astId": 12074, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "tokenLoan", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)" + }, + { + "astId": 12099, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "nonces", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_address,t_uint256)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_contract(IERC20)1405": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IERC721)17337": { + "encoding": "inplace", + "label": "contract IERC721", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct NFTPair.TokenLoan)", + "numberOfBytes": "32", + "value": "t_struct(TokenLoan)12070_storage" + }, + "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct TokenLoanParams)", + "numberOfBytes": "32", + "value": "t_struct(TokenLoanParams)11920_storage" + }, + "t_struct(TokenLoan)12070_storage": { + "encoding": "inplace", + "label": "struct NFTPair.TokenLoan", + "members": [ + { + "astId": 12063, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "borrower", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 12065, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "lender", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 12067, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "startTime", + "offset": 20, + "slot": "1", + "type": "t_uint64" + }, + { + "astId": 12069, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "status", + "offset": 28, + "slot": "1", + "type": "t_uint8" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenLoanParams)11920_storage": { + "encoding": "inplace", + "label": "struct TokenLoanParams", + "members": [ + { + "astId": 11915, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "valuation", + "offset": 0, + "slot": "0", + "type": "t_uint128" + }, + { + "astId": 11917, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "duration", + "offset": 16, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 11919, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "annualInterestBPS", + "offset": 24, + "slot": "0", + "type": "t_uint16" + } + ], + "numberOfBytes": "32" + }, + "t_uint128": { + "encoding": "inplace", + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint16": { + "encoding": "inplace", + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/NFTPairMock.json b/deployments/ropsten/NFTPairMock.json new file mode 100644 index 00000000..4708e587 --- /dev/null +++ b/deployments/ropsten/NFTPairMock.json @@ -0,0 +1,1179 @@ +{ + "address": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", + "abi": [ + { + "inputs": [ + { + "internalType": "contract IBentoBoxV1", + "name": "bentoBox_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "newFeeTo", + "type": "address" + } + ], + "name": "LogFeeTo", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "LogLend", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "LogRemoveCollateral", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "LogRepay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "name": "LogRequestLoan", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "name": "LogUpdateLoanParams", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "feeTo", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeShare", + "type": "uint256" + } + ], + "name": "LogWithdrawFees", + "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": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "asset", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bentoBox", + "outputs": [ + { + "internalType": "contract IBentoBoxV1", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "t", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "aprBPS", + "type": "uint16" + } + ], + "name": "calculateInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "interest", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "claimOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collateral", + "outputs": [ + { + "internalType": "contract IERC721", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8[]", + "name": "actions", + "type": "uint8[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "bytes[]", + "name": "datas", + "type": "bytes[]" + } + ], + "name": "cook", + "outputs": [ + { + "internalType": "uint256", + "name": "value1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value2", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "feeTo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feesEarnedShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "init", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "accepted", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "lend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "masterContract", + "outputs": [ + { + "internalType": "contract NFTPair", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "removeCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "repay", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skimCollateral", + "type": "bool" + }, + { + "internalType": "bool", + "name": "anyTokenId", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "requestAndBorrow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bool", + "name": "skim", + "type": "bool" + } + ], + "name": "requestLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newFeeTo", + "type": "address" + } + ], + "name": "setFeeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "skimFunds", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "takeCollateralAndLend", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokenLoan", + "outputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "internalType": "uint64", + "name": "startTime", + "type": "uint64" + }, + { + "internalType": "uint8", + "name": "status", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokenLoanParams", + "outputs": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + }, + { + "internalType": "bool", + "name": "direct", + "type": "bool" + }, + { + "internalType": "bool", + "name": "renounce", + "type": "bool" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint128", + "name": "valuation", + "type": "uint128" + }, + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "annualInterestBPS", + "type": "uint16" + } + ], + "internalType": "struct TokenLoanParams", + "name": "params", + "type": "tuple" + } + ], + "name": "updateLoanParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", + "transactionIndex": 13, + "gasUsed": "3505038", + "logsBloom": "0x00000000000000000000000000000000000000002000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000020000000000000000000800000000000002000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000001000000000000000000000004000000000000010000000", + "blockHash": "0xe18e183a1e987327f0e2b01c997bad9e64d3e07f1bf7640966f24d6ab9836392", + "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", + "logs": [ + { + "transactionIndex": 13, + "blockNumber": 12212438, + "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", + "address": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000063a1e3877b1662a9ad124f8611b06e3ffbc29cba" + ], + "data": "0x", + "logIndex": 7, + "blockHash": "0xe18e183a1e987327f0e2b01c997bad9e64d3e07f1bf7640966f24d6ab9836392" + } + ], + "blockNumber": 12212438, + "cumulativeGasUsed": "4102587", + "status": 1, + "byzantium": true + }, + "args": [ + "0x9A5620779feF1928eF87c1111491212efC2C3cB8" + ], + "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"bentoBox_\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"LogFeeTo\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogLend\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"LogRemoveCollateral\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogRepay\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogRequestLoan\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogUpdateLoanParams\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"feeTo\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"LogWithdrawFees\",\"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\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"asset\",\"outputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bentoBox\",\"outputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"principal\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"t\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"aprBPS\",\"type\":\"uint16\"}],\"name\":\"calculateInterest\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"interest\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"claimOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"collateral\",\"outputs\":[{\"internalType\":\"contract IERC721\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8[]\",\"name\":\"actions\",\"type\":\"uint8[]\"},{\"internalType\":\"uint256[]\",\"name\":\"values\",\"type\":\"uint256[]\"},{\"internalType\":\"bytes[]\",\"name\":\"datas\",\"type\":\"bytes[]\"}],\"name\":\"cook\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"value1\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"value2\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeTo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feesEarnedShare\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"init\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"accepted\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"lend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"masterContract\",\"outputs\":[{\"internalType\":\"contract NFTPair\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"removeCollateral\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"repay\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimCollateral\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"anyTokenId\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"requestAndBorrow\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"requestLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"setFeeTo\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimFunds\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"takeCollateralAndLend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoan\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"startTime\",\"type\":\"uint64\"},{\"internalType\":\"uint8\",\"name\":\"status\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoanParams\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"direct\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"renounce\",\"type\":\"bool\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"name\":\"updateLoanParams\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.\",\"kind\":\"dev\",\"methods\":{\"cook(uint8[],uint256[],bytes[])\":{\"params\":{\"actions\":\"An array with a sequence of actions to execute (see ACTION_ declarations).\",\"datas\":\"A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\",\"values\":\"A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\"},\"returns\":{\"value1\":\"May contain the first positioned return value of the last executed action (if applicable).\",\"value2\":\"May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\"}},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"params\":{\"accepted\":\"Loan parameters as the lender saw them, for security\",\"skim\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"removeCollateral(uint256,address)\":{\"params\":{\"to\":\"The receiver of the token.\",\"tokenId\":\"The token\"}},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"anyTokenId\":\"Set if lender agreed to any token. Must have tokenId 0 in signature.\",\"lender\":\"Lender, whose BentoBox balance the funds will come from\",\"params\":\"Loan parameters requested, and signed by the lender\",\"recipient\":\"Address to receive the loan.\",\"skimCollateral\":\"True if the collateral has already been transfered\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"params\":{\"params\":\"Loan conditions on offer\",\"skim\":\"True if the token has already been transfered\",\"to\":\"Address to receive the loan, or option to withdraw collateral\",\"tokenId\":\"ID of the NFT\"}},\"setFeeTo(address)\":{\"params\":{\"newFeeTo\":\"The address of the receiver.\"}},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"borrower\":\"Address that provides collateral and receives the loan\",\"params\":\"Loan terms offered, and signed by the borrower\",\"skimFunds\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"transferOwnership(address,bool,bool)\":{\"params\":{\"direct\":\"True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\",\"newOwner\":\"Address of the new owner.\",\"renounce\":\"Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\"}}},\"title\":\"NFTPair\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"calculateInterest(uint256,uint64,uint16)\":{\"notice\":\"Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected.\"},\"claimOwnership()\":{\"notice\":\"Needs to be called by `pendingOwner` to claim ownership.\"},\"constructor\":\"The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.\",\"cook(uint8[],uint256[],bytes[])\":{\"notice\":\"Executes a set of actions and allows composability (contract calls) to other contracts.\"},\"init(bytes)\":{\"notice\":\"De facto constructor for clone contracts\"},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"notice\":\"Lends with the parameters specified by the borrower.\"},\"removeCollateral(uint256,address)\":{\"notice\":\"Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan.\"},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Caller provides collateral; loan can go to a different address.\"},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"notice\":\"Deposit an NFT as collateral and request a loan against it\"},\"setFeeTo(address)\":{\"notice\":\"Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function.\"},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party.\"},\"transferOwnership(address,bool,bool)\":{\"notice\":\"Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`.\"},\"withdrawFees()\":{\"notice\":\"Withdraws the fees accumulated.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/NFTPair.sol\":\"NFTPair\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\\n// Edited by BoringCrypto\\n\\ncontract BoringOwnableData {\\n address public owner;\\n address public pendingOwner;\\n}\\n\\ncontract BoringOwnable is BoringOwnableData {\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /// @notice `owner` defaults to msg.sender on construction.\\n constructor() public {\\n owner = msg.sender;\\n emit OwnershipTransferred(address(0), msg.sender);\\n }\\n\\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\\n /// Can only be invoked by the current `owner`.\\n /// @param newOwner Address of the new owner.\\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\\n function transferOwnership(\\n address newOwner,\\n bool direct,\\n bool renounce\\n ) public onlyOwner {\\n if (direct) {\\n // Checks\\n require(newOwner != address(0) || renounce, \\\"Ownable: zero address\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, newOwner);\\n owner = newOwner;\\n pendingOwner = address(0);\\n } else {\\n // Effects\\n pendingOwner = newOwner;\\n }\\n }\\n\\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\\n function claimOwnership() public {\\n address _pendingOwner = pendingOwner;\\n\\n // Checks\\n require(msg.sender == _pendingOwner, \\\"Ownable: caller != pending owner\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, _pendingOwner);\\n owner = _pendingOwner;\\n pendingOwner = address(0);\\n }\\n\\n /// @notice Only allows the `owner` to execute the function.\\n modifier onlyOwner() {\\n require(msg.sender == owner, \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n}\\n\",\"keccak256\":\"0xbde1619421fef865bf5f5f806e319900fb862e27f0aef6e0878e93f04f477601\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IMasterContract {\\n /// @notice Init function that gets called from `BoringFactory.deploy`.\\n /// Also kown as the constructor for cloned contracts.\\n /// Any ETH send to `BoringFactory.deploy` ends up here.\\n /// @param data Can be abi encoded arguments or anything else.\\n function init(bytes calldata data) external payable;\\n}\\n\",\"keccak256\":\"0xc8d7519d2bd26fc6d5125f8fc3fe2a6aada76f71f26b4712e0a4160f1cbdb2ba\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"../interfaces/IERC20.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nlibrary BoringERC20 {\\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\\n\\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\\n if (data.length >= 64) {\\n return abi.decode(data, (string));\\n } else if (data.length == 32) {\\n uint8 i = 0;\\n while (i < 32 && data[i] != 0) {\\n i++;\\n }\\n bytes memory bytesArray = new bytes(i);\\n for (i = 0; i < 32 && data[i] != 0; i++) {\\n bytesArray[i] = data[i];\\n }\\n return string(bytesArray);\\n } else {\\n return \\\"???\\\";\\n }\\n }\\n\\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token symbol.\\n function safeSymbol(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token name.\\n function safeName(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (uint8) Token decimals.\\n function safeDecimals(IERC20 token) internal view returns (uint8) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\\n }\\n\\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\\n /// @param token The address of the ERC-20 token.\\n /// @param to The address of the user to check.\\n /// @return amount The token amount.\\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\\n require(success && data.length >= 32, \\\"BoringERC20: BalanceOf failed\\\");\\n amount = abi.decode(data, (uint256));\\n }\\n\\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransfer(\\n IERC20 token,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: Transfer failed\\\");\\n }\\n\\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param from Transfer tokens from.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransferFrom(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: TransferFrom failed\\\");\\n }\\n}\\n\",\"keccak256\":\"0xc0b0529bf740b422941fc4899762ef3bde7d05a56b1cdb60b063c2aa63883d65\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./BoringMath.sol\\\";\\n\\nstruct Rebase {\\n uint128 elastic;\\n uint128 base;\\n}\\n\\n/// @notice A rebasing library using overflow-/underflow-safe math.\\nlibrary RebaseLibrary {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n\\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\\n function toBase(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (uint256 base) {\\n if (total.elastic == 0) {\\n base = elastic;\\n } else {\\n base = elastic.mul(total.base) / total.elastic;\\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\\n base = base.add(1);\\n }\\n }\\n }\\n\\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\\n function toElastic(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (uint256 elastic) {\\n if (total.base == 0) {\\n elastic = base;\\n } else {\\n elastic = base.mul(total.elastic) / total.base;\\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\\n elastic = elastic.add(1);\\n }\\n }\\n }\\n\\n /// @notice Add `elastic` to `total` and doubles `total.base`.\\n /// @return (Rebase) The new total.\\n /// @return base in relationship to `elastic`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 base) {\\n base = toBase(total, elastic, roundUp);\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return (total, base);\\n }\\n\\n /// @notice Sub `base` from `total` and update `total.elastic`.\\n /// @return (Rebase) The new total.\\n /// @return elastic in relationship to `base`.\\n function sub(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 elastic) {\\n elastic = toElastic(total, base, roundUp);\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return (total, elastic);\\n }\\n\\n /// @notice Add `elastic` and `base` to `total`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return total;\\n }\\n\\n /// @notice Subtract `elastic` and `base` to `total`.\\n function sub(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return total;\\n }\\n\\n /// @notice Add `elastic` to `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.add(elastic.to128());\\n }\\n\\n /// @notice Subtract `elastic` from `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\\n }\\n}\\n\",\"keccak256\":\"0xab228bfa8a3019a4f7effa8aeeb05de141d328703d8a2f7b87ca811d0ca33196\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IBatchFlashBorrower {\\n function onBatchFlashLoan(\\n address sender,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n uint256[] calldata fees,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x825a46e61443df6e1289b513da4386d0413d0b5311553f3e7e7e5c90412ddd5d\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\\nimport './IBatchFlashBorrower.sol';\\nimport './IFlashBorrower.sol';\\nimport './IStrategy.sol';\\n\\ninterface IBentoBoxV1 {\\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\\n event LogRegisterProtocol(address indexed protocol);\\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\\n event LogStrategyDivest(address indexed token, uint256 amount);\\n event LogStrategyInvest(address indexed token, uint256 amount);\\n event LogStrategyLoss(address indexed token, uint256 amount);\\n event LogStrategyProfit(address indexed token, uint256 amount);\\n event LogStrategyQueued(address indexed token, address indexed strategy);\\n event LogStrategySet(address indexed token, address indexed strategy);\\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n function balanceOf(IERC20, address) external view returns (uint256);\\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\\n function claimOwnership() external;\\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\\n function masterContractApproved(address, address) external view returns (bool);\\n function masterContractOf(address) external view returns (address);\\n function nonces(address) external view returns (uint256);\\n function owner() external view returns (address);\\n function pendingOwner() external view returns (address);\\n function pendingStrategy(IERC20) external view returns (IStrategy);\\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\\n function registerProtocol() external;\\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\\n function strategy(IERC20) external view returns (IStrategy);\\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\\n function totals(IERC20) external view returns (Rebase memory totals_);\\n function transfer(IERC20 token, address from, address to, uint256 share) external;\\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\\n function whitelistMasterContract(address masterContract, bool approved) external;\\n function whitelistedMasterContracts(address) external view returns (bool);\\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\\n}\",\"keccak256\":\"0x9c025e34e0ef0c1fc9372ada9afa61925341ee93de9b9a79e77de55d715b6fb6\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IFlashBorrower {\\n function onFlashLoan(\\n address sender,\\n IERC20 token,\\n uint256 amount,\\n uint256 fee,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x6e389a5acb7b3e7f337b7e28477e998228f05fc4c8ff877eab32d3e15037ccc2\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IStrategy {\\n // Send the assets to the Strategy and call skim to invest them\\n function skim(uint256 amount) external;\\n\\n // Harvest any profits made converted to the asset and pass them to the caller\\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\\n\\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\\n\\n // Withdraw all assets in the safest way possible. This shouldn't fail.\\n function exit(uint256 balance) external returns (int256 amountAdded);\\n}\",\"keccak256\":\"0x91c02244e1508cf8e4d6c45110c57142301c237e809dcad67b8022f83555ba13\",\"license\":\"MIT\"},\"contracts/NFTPair.sol\":{\"content\":\"// SPDX-License-Identifier: UNLICENSED\\n\\n// Private Pool (NFT collateral)\\n\\n// ( ( (\\n// )\\\\ ) ( )\\\\ )\\\\ ) (\\n// (((_) ( /( ))\\\\ ((_)(()/( )( ( (\\n// )\\\\___ )(_)) /((_) _ ((_))(()\\\\ )\\\\ )\\\\ )\\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\\n// | (__ / _` || || || |/ _` | | '_|/ _ \\\\| ' \\\\))\\n// \\\\___|\\\\__,_| \\\\_,_||_|\\\\__,_| |_| \\\\___/|_||_|\\n\\n// Copyright (c) 2021 BoringCrypto - All rights reserved\\n// Twitter: @Boring_Crypto\\n\\n// Special thanks to:\\n// @0xKeno - for all his invaluable contributions\\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\\n\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/Domain.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\\\";\\nimport \\\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\\\";\\nimport \\\"./interfaces/IERC721.sol\\\";\\n\\nstruct TokenLoanParams {\\n uint128 valuation; // How much will you get? OK to owe until expiration.\\n uint64 duration; // Length of loan in seconds\\n uint16 annualInterestBPS; // Variable cost of taking out the loan\\n}\\n\\ninterface ILendingClub {\\n // Per token settings.\\n function willLend(uint256 tokenId, TokenLoanParams memory params)\\n external\\n view\\n returns (bool);\\n\\n function lendingConditions(address nftPair, uint256 tokenId)\\n external\\n view\\n returns (TokenLoanParams memory);\\n}\\n\\ninterface INFTPair {\\n function collateral() external view returns (IERC721);\\n\\n function asset() external view returns (IERC20);\\n\\n function masterContract() external view returns (address);\\n\\n function bentoBox() external view returns (IBentoBoxV1);\\n\\n function removeCollateral(uint256 tokenId, address to) external;\\n}\\n\\n/// @title NFTPair\\n/// @dev This contract allows contract calls to any contract (except BentoBox)\\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n using RebaseLibrary for Rebase;\\n using BoringERC20 for IERC20;\\n\\n event LogRequestLoan(\\n address indexed borrower,\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n event LogUpdateLoanParams(\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n // This automatically clears the associated loan, if any\\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\\n // Details are in the loan request\\n event LogLend(address indexed lender, uint256 indexed tokenId);\\n event LogRepay(address indexed from, uint256 indexed tokenId);\\n event LogFeeTo(address indexed newFeeTo);\\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\\n\\n // Immutables (for MasterContract and all clones)\\n IBentoBoxV1 public immutable bentoBox;\\n NFTPair public immutable masterContract;\\n\\n // MasterContract variables\\n address public feeTo;\\n\\n // Per clone variables\\n // Clone init settings\\n IERC721 public collateral;\\n IERC20 public asset;\\n\\n // A note on terminology:\\n // \\\"Shares\\\" are BentoBox shares.\\n\\n // Track assets we own. Used to allow skimming the excesss.\\n uint256 public feesEarnedShare;\\n\\n // Per token settings.\\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\\n\\n uint8 private constant LOAN_INITIAL = 0;\\n uint8 private constant LOAN_REQUESTED = 1;\\n uint8 private constant LOAN_OUTSTANDING = 2;\\n struct TokenLoan {\\n address borrower;\\n address lender;\\n uint64 startTime;\\n uint8 status;\\n }\\n mapping(uint256 => TokenLoan) public tokenLoan;\\n\\n // Do not go over 100% on either of these..\\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\\n uint256 private constant OPEN_FEE_BPS = 100;\\n uint256 private constant BPS = 10_000;\\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\\n\\n // Highest order term in the Maclaurin series for exp used by\\n // `calculateIntest`.\\n // Intuitive interpretation: interest continuously accrues on the principal.\\n // That interest, in turn, earns \\\"second-order\\\" interest-on-interest, which\\n // itself earns \\\"third-order\\\" interest, etc. This constant determines how\\n // far we take this until we stop counting.\\n //\\n // The error, in terms of the interest rate, is at least\\n //\\n // ----- n ----- Infinity\\n // \\\\ x^k \\\\ x^k\\n // e^x - ) --- , which is ) --- ,\\n // / k! / k!\\n // ----- k = 1 k ----- k = n + 1\\n //\\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\\n // interest that is owed at rate r over time t. It makes no difference if\\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\\n // is the same. Why \\\"at least\\\"? There are also rounding errors. See\\n // `calculateInterest` for more detail.\\n // The factorial in the denominator \\\"wins\\\"; for all reasonable (and quite\\n // a few unreasonable) interest rates, the lower-order terms contribute the\\n // most to the total. The following table lists some of the calculated\\n // approximations for different values of n, along with the \\\"true\\\" result:\\n //\\n // Total: 10% 20% 50% 100% 200% 500% 1000%\\n // -----------------------------------------------------------------------\\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\\n //\\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\\n //\\n // For instance, calculating the compounding effects of 200% in \\\"total\\\"\\n // interest to the sixth order results in 635.6%, whereas the true result\\n // is 638.9%.\\n // At 500% that difference is a little more dramatic, but it is still in\\n // the same ballpark -- and of little practical consequence unless the\\n // collateral can be expected to go up more than 112 times in value.\\n // Still, for volatile tokens, or an asset that is somehow known to be very\\n // inflationary, use a different number.\\n // Zero (no interest at all) is ignored and treated as one (linear only).\\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\\n\\n // For signed lend / borrow requests:\\n mapping(address => uint256) public nonces;\\n\\n /// @notice The constructor is only used for the initial master contract.\\n /// @notice Subsequent clones are initialised via `init`.\\n constructor(IBentoBoxV1 bentoBox_) public {\\n bentoBox = bentoBox_;\\n masterContract = this;\\n }\\n\\n /// @notice De facto constructor for clone contracts\\n function init(bytes calldata data) public payable override {\\n require(\\n address(collateral) == address(0),\\n \\\"NFTPair: already initialized\\\"\\n );\\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\\n require(address(collateral) != address(0), \\\"NFTPair: bad pair\\\");\\n }\\n\\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params)\\n public\\n {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_OUTSTANDING) {\\n // The lender can change terms so long as the changes are strictly\\n // the same or better for the borrower:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\\n require(\\n params.duration >= cur.duration &&\\n params.valuation <= cur.valuation &&\\n params.annualInterestBPS <= cur.annualInterestBPS,\\n \\\"NFTPair: worse params\\\"\\n );\\n } else if (loan.status == LOAN_REQUESTED) {\\n // The borrower has already deposited the collateral and can\\n // change whatever they like\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else {\\n // The loan has not been taken out yet; the borrower needs to\\n // provide collateral.\\n revert(\\\"NFTPair: no collateral\\\");\\n }\\n tokenLoanParams[tokenId] = params;\\n emit LogUpdateLoanParams(\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n function _requestLoan(\\n address collateralProvider,\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) private {\\n // Edge case: valuation can be zero. That effectively gifts the NFT and\\n // is therefore a bad idea, but does not break the contract.\\n require(\\n tokenLoan[tokenId].status == LOAN_INITIAL,\\n \\\"NFTPair: loan exists\\\"\\n );\\n if (skim) {\\n require(\\n collateral.ownerOf(tokenId) == address(this),\\n \\\"NFTPair: skim failed\\\"\\n );\\n } else {\\n collateral.transferFrom(collateralProvider, address(this), tokenId);\\n }\\n TokenLoan memory loan;\\n loan.borrower = to;\\n loan.status = LOAN_REQUESTED;\\n tokenLoan[tokenId] = loan;\\n tokenLoanParams[tokenId] = params;\\n\\n emit LogRequestLoan(\\n to,\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n /// @notice Deposit an NFT as collateral and request a loan against it\\n /// @param tokenId ID of the NFT\\n /// @param to Address to receive the loan, or option to withdraw collateral\\n /// @param params Loan conditions on offer\\n /// @param skim True if the token has already been transfered\\n function requestLoan(\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) public {\\n _requestLoan(msg.sender, tokenId, params, to, skim);\\n }\\n\\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\\n /// @notice This destroys the loan.\\n /// @param tokenId The token\\n /// @param to The receiver of the token.\\n function removeCollateral(uint256 tokenId, address to) public {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_REQUESTED) {\\n // We are withdrawing collateral that is not in use:\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else if (loan.status == LOAN_OUTSTANDING) {\\n // We are seizing collateral as the lender. The loan has to be\\n // expired and not paid off:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <=\\n block.timestamp,\\n \\\"NFTPair: not expired\\\"\\n );\\n }\\n // If there somehow is collateral but no accompanying loan, then anyone\\n // can claim it by first requesting a loan with `skim` set to true, and\\n // then withdrawing. So we might as well allow it here..\\n delete tokenLoan[tokenId];\\n collateral.transferFrom(address(this), to, tokenId);\\n emit LogRemoveCollateral(tokenId, to);\\n }\\n\\n // Assumes the lender has agreed to the loan.\\n function _lend(\\n address lender,\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) internal {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_REQUESTED, \\\"NFTPair: not available\\\");\\n TokenLoanParams memory params = tokenLoanParams[tokenId];\\n\\n // Valuation has to be an exact match, everything else must be at least\\n // as good for the lender as `accepted`.\\n require(\\n params.valuation == accepted.valuation &&\\n params.duration <= accepted.duration &&\\n params.annualInterestBPS >= accepted.annualInterestBPS,\\n \\\"NFTPair: bad params\\\"\\n );\\n\\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\\n\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare -\\n openFeeShare +\\n protocolFeeShare +\\n feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n } else {\\n bentoBox.transfer(\\n asset,\\n lender,\\n address(this),\\n totalShare - openFeeShare + protocolFeeShare\\n );\\n }\\n // No underflow: follows from OPEN_FEE_BPS <= BPS\\n uint256 borrowerShare = totalShare - openFeeShare;\\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\\n // No overflow: addends (and result) must fit in BentoBox\\n feesEarnedShare += protocolFeeShare;\\n\\n loan.lender = lender;\\n loan.status = LOAN_OUTSTANDING;\\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\\n tokenLoan[tokenId] = loan;\\n\\n emit LogLend(lender, tokenId);\\n }\\n\\n /// @notice Lends with the parameters specified by the borrower.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param accepted Loan parameters as the lender saw them, for security\\n /// @param skim True if the funds have been transfered to the contract\\n function lend(\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) public {\\n _lend(msg.sender, tokenId, accepted, skim);\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // NOTE on signature hashes: the domain separator only guarantees that the\\n // chain ID and master contract are a match, so we explicitly include the\\n // clone address (and the asset/collateral addresses):\\n\\n // keccak256(\\\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant LEND_SIGNATURE_HASH =\\n 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\\n\\n // keccak256(\\\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant BORROW_SIGNATURE_HASH =\\n 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\\n\\n /// @notice Request and immediately borrow from a pre-committed lender\\n\\n /// @notice Caller provides collateral; loan can go to a different address.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param lender Lender, whose BentoBox balance the funds will come from\\n /// @param recipient Address to receive the loan.\\n /// @param params Loan parameters requested, and signed by the lender\\n /// @param skimCollateral True if the collateral has already been transfered\\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\\n function requestAndBorrow(\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\\n require(\\n ILendingClub(lender).willLend(tokenId, params),\\n \\\"NFTPair: LendingClub does not like you\\\"\\n );\\n } else {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[lender]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n LEND_SIGNATURE_HASH,\\n address(this),\\n anyTokenId ? 0 : tokenId,\\n anyTokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == lender,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n }\\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\\n _lend(lender, tokenId, params, false);\\n }\\n\\n /// @notice Take collateral from a pre-commited borrower and lend against it\\n /// @notice Collateral must come from the borrower, not a third party.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param borrower Address that provides collateral and receives the loan\\n /// @param params Loan terms offered, and signed by the borrower\\n /// @param skimFunds True if the funds have been transfered to the contract\\n function takeCollateralAndLend(\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[borrower]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n BORROW_SIGNATURE_HASH,\\n address(this),\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == borrower,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n _requestLoan(borrower, tokenId, params, borrower, false);\\n _lend(msg.sender, tokenId, params, skimFunds);\\n }\\n\\n /// Approximates continuous compounding. Uses Horner's method to evaluate\\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\\n /// errors along the way. The following is always guaranteed:\\n ///\\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\\n ///\\n /// where time = t/YEAR, up to at most the rounding error obtained in\\n /// calculating linear interest.\\n ///\\n /// If the theoretical result that we are approximating (the rightmost part\\n /// of the above inquality) fits in 128 bits, then the function is\\n /// guaranteed not to revert (unless n > 250, which is way too high).\\n /// If even the linear interest (leftmost part of the inequality) does not\\n /// the function will revert.\\n /// Otherwise, the function may revert, return a reasonable result, or\\n /// return a very inaccurate result. Even then the above inequality is\\n /// respected.\\n function calculateInterest(\\n uint256 principal,\\n uint64 t,\\n uint16 aprBPS\\n ) public pure returns (uint256 interest) {\\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\\n //\\n // We calculate\\n //\\n // ----- n ----- n\\n // \\\\ principal * (t * aprBPS)^k \\\\\\n // ) -------------------------- =: ) term_k\\n // / k! * YEAR_BPS^k /\\n // ----- k = 1 ----- k = 1\\n //\\n // which approaches, but never exceeds the \\\"theoretical\\\" result,\\n //\\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\\n //\\n // as n goes to infinity. We use the fact that\\n //\\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\\n // term_k = ---------------------------------------------\\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\\n //\\n // t * aprBPS\\n // = term_{k-1} * ------------ (*)\\n // k * YEAR_BPS\\n //\\n // to calculate the terms one by one. The principal affords us the\\n // precision to carry out the division without resorting to fixed-point\\n // math. Any rounding error is downward, which we consider acceptable.\\n //\\n // Since all numbers involved are positive, each term is certainly\\n // bounded above by M. From (*) we see that any intermediate results\\n // are at most\\n //\\n // denom_k := k * YEAR_BPS.\\n //\\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\\n // which proves that all calculations will certainly not overflow if M\\n // fits in 128 bits.\\n //\\n // If M does not fit, then the intermediate results for some term may\\n // eventually overflow, but this cannot happen at the first term, and\\n // neither can the total overflow because it uses checked math.\\n //\\n // This constitutes a guarantee of specified behavior when M >= 2^128.\\n uint256 x = uint256(t) * aprBPS;\\n uint256 term_k = (principal * x) / YEAR_BPS;\\n uint256 denom_k = YEAR_BPS;\\n\\n interest = term_k;\\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\\n denom_k += YEAR_BPS;\\n term_k = (term_k * x) / denom_k;\\n interest = interest.add(term_k); // <- Only overflow check we need\\n }\\n\\n if (interest >= 2**128) {\\n revert();\\n }\\n }\\n\\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_OUTSTANDING, \\\"NFTPair: no loan\\\");\\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\\n \\\"NFTPair: loan expired\\\"\\n );\\n\\n uint128 principal = loanParams.valuation;\\n\\n // No underflow: loan.startTime is only ever set to a block timestamp\\n // Cast is safe: if this overflows, then all loans have expired anyway\\n uint256 interest = calculateInterest(\\n principal,\\n uint64(block.timestamp - loan.startTime),\\n loanParams.annualInterestBPS\\n ).to128();\\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\\n amount = principal + interest;\\n\\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\\n\\n address from;\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare + feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n from = address(this);\\n // No overflow: result fits in BentoBox\\n } else {\\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\\n from = msg.sender;\\n }\\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\\n feesEarnedShare += feeShare;\\n delete tokenLoan[tokenId];\\n\\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\\n collateral.transferFrom(address(this), loan.borrower, tokenId);\\n\\n emit LogRepay(from, tokenId);\\n }\\n\\n uint8 internal constant ACTION_REPAY = 2;\\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\\n\\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\\n uint8 internal constant ACTION_LEND = 13;\\n\\n // Function on BentoBox\\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\\n\\n // Any external call (except to BentoBox)\\n uint8 internal constant ACTION_CALL = 30;\\n\\n // Signed requests\\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\\n\\n int256 internal constant USE_VALUE1 = -1;\\n int256 internal constant USE_VALUE2 = -2;\\n\\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\\n function _num(\\n int256 inNum,\\n uint256 value1,\\n uint256 value2\\n ) internal pure returns (uint256 outNum) {\\n outNum = inNum >= 0\\n ? uint256(inNum)\\n : (inNum == USE_VALUE1 ? value1 : value2);\\n }\\n\\n /// @dev Helper function for depositing into `bentoBox`.\\n function _bentoDeposit(\\n bytes memory data,\\n uint256 value,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\\n share = int256(_num(share, value1, value2));\\n return\\n bentoBox.deposit{value: value}(\\n token,\\n msg.sender,\\n to,\\n uint256(amount),\\n uint256(share)\\n );\\n }\\n\\n /// @dev Helper function to withdraw from the `bentoBox`.\\n function _bentoWithdraw(\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n return\\n bentoBox.withdraw(\\n token,\\n msg.sender,\\n to,\\n _num(amount, value1, value2),\\n _num(share, value1, value2)\\n );\\n }\\n\\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\\n /// This also means that calls made from this contract shall *not* be trusted.\\n function _call(\\n uint256 value,\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (bytes memory, uint8) {\\n (\\n address callee,\\n bytes memory callData,\\n bool useValue1,\\n bool useValue2,\\n uint8 returnValues\\n ) = abi.decode(data, (address, bytes, bool, bool, uint8));\\n\\n if (useValue1 && !useValue2) {\\n callData = abi.encodePacked(callData, value1);\\n } else if (!useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value2);\\n } else if (useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value1, value2);\\n }\\n\\n require(\\n callee != address(bentoBox) &&\\n callee != address(collateral) &&\\n callee != address(this),\\n \\\"NFTPair: can't call\\\"\\n );\\n\\n (bool success, bytes memory returnData) = callee.call{value: value}(\\n callData\\n );\\n require(success, \\\"NFTPair: call failed\\\");\\n return (returnData, returnValues);\\n }\\n\\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\\n function cook(\\n uint8[] calldata actions,\\n uint256[] calldata values,\\n bytes[] calldata datas\\n ) external payable returns (uint256 value1, uint256 value2) {\\n for (uint256 i = 0; i < actions.length; i++) {\\n uint8 action = actions[i];\\n if (action == ACTION_REPAY) {\\n (uint256 tokenId, bool skim) = abi.decode(\\n datas[i],\\n (uint256, bool)\\n );\\n repay(tokenId, skim);\\n } else if (action == ACTION_REMOVE_COLLATERAL) {\\n (uint256 tokenId, address to) = abi.decode(\\n datas[i],\\n (uint256, address)\\n );\\n removeCollateral(tokenId, to);\\n } else if (action == ACTION_REQUEST_LOAN) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) = abi.decode(\\n datas[i],\\n (uint256, TokenLoanParams, address, bool)\\n );\\n requestLoan(tokenId, params, to, skim);\\n } else if (action == ACTION_LEND) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n bool skim\\n ) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\\n lend(tokenId, params, skim);\\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\\n (\\n address user,\\n address _masterContract,\\n bool approved,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (address, address, bool, uint8, bytes32, bytes32)\\n );\\n bentoBox.setMasterContractApproval(\\n user,\\n _masterContract,\\n approved,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_BENTO_DEPOSIT) {\\n (value1, value2) = _bentoDeposit(\\n datas[i],\\n values[i],\\n value1,\\n value2\\n );\\n } else if (action == ACTION_BENTO_WITHDRAW) {\\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\\n } else if (action == ACTION_BENTO_TRANSFER) {\\n (IERC20 token, address to, int256 share) = abi.decode(\\n datas[i],\\n (IERC20, address, int256)\\n );\\n bentoBox.transfer(\\n token,\\n msg.sender,\\n to,\\n _num(share, value1, value2)\\n );\\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\\n (\\n IERC20 token,\\n address[] memory tos,\\n uint256[] memory shares\\n ) = abi.decode(datas[i], (IERC20, address[], uint256[]));\\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\\n } else if (action == ACTION_CALL) {\\n (bytes memory returnData, uint8 returnValues) = _call(\\n values[i],\\n datas[i],\\n value1,\\n value2\\n );\\n\\n if (returnValues == 1) {\\n (value1) = abi.decode(returnData, (uint256));\\n } else if (returnValues == 2) {\\n (value1, value2) = abi.decode(\\n returnData,\\n (uint256, uint256)\\n );\\n }\\n } else if (action == ACTION_REQUEST_AND_BORROW) {\\n (\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n address,\\n TokenLoanParams,\\n bool,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n requestAndBorrow(\\n tokenId,\\n lender,\\n recipient,\\n params,\\n skimCollateral,\\n anyTokenId,\\n deadline,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\\n (\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n TokenLoanParams,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n takeCollateralAndLend(\\n tokenId,\\n borrower,\\n params,\\n skimFunds,\\n deadline,\\n v,\\n r,\\n s\\n );\\n }\\n }\\n }\\n\\n /// @notice Withdraws the fees accumulated.\\n function withdrawFees() public {\\n address to = masterContract.feeTo();\\n\\n uint256 _share = feesEarnedShare;\\n if (_share > 0) {\\n bentoBox.transfer(asset, address(this), to, _share);\\n feesEarnedShare = 0;\\n }\\n\\n emit LogWithdrawFees(to, _share);\\n }\\n\\n /// @notice Sets the beneficiary of fees accrued in liquidations.\\n /// MasterContract Only Admin function.\\n /// @param newFeeTo The address of the receiver.\\n function setFeeTo(address newFeeTo) public onlyOwner {\\n feeTo = newFeeTo;\\n emit LogFeeTo(newFeeTo);\\n }\\n}\\n\",\"keccak256\":\"0x2f116b73330c8b296bd38f699afc82f62753a7ced6b6a85a3f4e0cc717c4351c\",\"license\":\"UNLICENSED\"},\"contracts/interfaces/IERC165.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.0 <0.8.0;\\n\\n/**\\n * @dev Interface of the ERC165 standard, as defined in the\\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\\n *\\n * Implementers can declare support of contract interfaces, which can then be\\n * queried by others ({ERC165Checker}).\\n *\\n * For an implementation, see {ERC165}.\\n */\\ninterface IERC165 {\\n /**\\n * @dev Returns true if this contract implements the interface defined by\\n * `interfaceId`. See the corresponding\\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\\n * to learn more about how these ids are created.\\n *\\n * This function call must use less than 30 000 gas.\\n */\\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\\n}\\n\",\"keccak256\":\"0x568574015c35b45a03f3bc3857240fb9985380d3faa3df7207123620d48ffe13\",\"license\":\"MIT\"},\"contracts/interfaces/IERC721.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.2 <0.8.0;\\n\\nimport \\\"./IERC165.sol\\\";\\n\\n/**\\n * @dev Required interface of an ERC721 compliant contract.\\n */\\ninterface IERC721 is IERC165 {\\n /**\\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\\n */\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\\n */\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\\n */\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n /**\\n * @dev Returns the number of tokens in ``owner``'s account.\\n */\\n function balanceOf(address owner) external view returns (uint256 balance);\\n\\n /**\\n * @dev Returns the owner of the `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function ownerOf(uint256 tokenId) external view returns (address owner);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Transfers `tokenId` token from `from` to `to`.\\n *\\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\\n * The approval is cleared when the token is transferred.\\n *\\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\\n *\\n * Requirements:\\n *\\n * - The caller must own the token or be an approved operator.\\n * - `tokenId` must exist.\\n *\\n * Emits an {Approval} event.\\n */\\n function approve(address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Returns the account approved for `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function getApproved(uint256 tokenId) external view returns (address operator);\\n\\n /**\\n * @dev Approve or remove `operator` as an operator for the caller.\\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\\n *\\n * Requirements:\\n *\\n * - The `operator` cannot be the caller.\\n *\\n * Emits an {ApprovalForAll} event.\\n */\\n function setApprovalForAll(address operator, bool _approved) external;\\n\\n /**\\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\\n *\\n * See {setApprovalForAll}\\n */\\n function isApprovedForAll(address owner, address operator) external view returns (bool);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\\n}\\n\",\"keccak256\":\"0x0bfe878a4a6ddcdba3d5b53a21e76bcb84bc77a114fbd432a5533bda12f155fa\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x6101006040523480156200001257600080fd5b5060405162003fdf38038062003fdf8339810160408190526200003591620000fd565b600080546001600160a01b0319163390811782556040519091907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a34660a08190526200008581620000a7565b608052506001600160601b0319606091821b1660c05230901b60e0526200014c565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692188230604051602001620000e0939291906200012d565b604051602081830303815290604052805190602001209050919050565b6000602082840312156200010f578081fd5b81516001600160a01b038116811462000126578182fd5b9392505050565b92835260208301919091526001600160a01b0316604082015260600190565b60805160a05160c05160601c60e05160601c613e02620001dd600039806108ba5280611b825250806109705280610d115280610ed95280610fb152806111d7528061160352806116af5280611764528061182b52806118f35280611f115280611fed528061219c52806126fc52806127c5528061288a528061291d525080611e75525080611eaa5250613e026000f3fe6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", + "devdoc": { + "details": "This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.", + "kind": "dev", + "methods": { + "cook(uint8[],uint256[],bytes[])": { + "params": { + "actions": "An array with a sequence of actions to execute (see ACTION_ declarations).", + "datas": "A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.", + "values": "A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`." + }, + "returns": { + "value1": "May contain the first positioned return value of the last executed action (if applicable).", + "value2": "May contain the second positioned return value of the last executed action which returns 2 values (if applicable)." + } + }, + "lend(uint256,(uint128,uint64,uint16),bool)": { + "params": { + "accepted": "Loan parameters as the lender saw them, for security", + "skim": "True if the funds have been transfered to the contract", + "tokenId": "ID of the token that will function as collateral" + } + }, + "removeCollateral(uint256,address)": { + "params": { + "to": "The receiver of the token.", + "tokenId": "The token" + } + }, + "requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)": { + "params": { + "anyTokenId": "Set if lender agreed to any token. Must have tokenId 0 in signature.", + "lender": "Lender, whose BentoBox balance the funds will come from", + "params": "Loan parameters requested, and signed by the lender", + "recipient": "Address to receive the loan.", + "skimCollateral": "True if the collateral has already been transfered", + "tokenId": "ID of the token that will function as collateral" + } + }, + "requestLoan(uint256,(uint128,uint64,uint16),address,bool)": { + "params": { + "params": "Loan conditions on offer", + "skim": "True if the token has already been transfered", + "to": "Address to receive the loan, or option to withdraw collateral", + "tokenId": "ID of the NFT" + } + }, + "setFeeTo(address)": { + "params": { + "newFeeTo": "The address of the receiver." + } + }, + "takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)": { + "params": { + "borrower": "Address that provides collateral and receives the loan", + "params": "Loan terms offered, and signed by the borrower", + "skimFunds": "True if the funds have been transfered to the contract", + "tokenId": "ID of the token that will function as collateral" + } + }, + "transferOwnership(address,bool,bool)": { + "params": { + "direct": "True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.", + "newOwner": "Address of the new owner.", + "renounce": "Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise." + } + } + }, + "title": "NFTPair", + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "calculateInterest(uint256,uint64,uint16)": { + "notice": "Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected." + }, + "claimOwnership()": { + "notice": "Needs to be called by `pendingOwner` to claim ownership." + }, + "constructor": "The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.", + "cook(uint8[],uint256[],bytes[])": { + "notice": "Executes a set of actions and allows composability (contract calls) to other contracts." + }, + "init(bytes)": { + "notice": "De facto constructor for clone contracts" + }, + "lend(uint256,(uint128,uint64,uint16),bool)": { + "notice": "Lends with the parameters specified by the borrower." + }, + "removeCollateral(uint256,address)": { + "notice": "Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan." + }, + "requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)": { + "notice": "Caller provides collateral; loan can go to a different address." + }, + "requestLoan(uint256,(uint128,uint64,uint16),address,bool)": { + "notice": "Deposit an NFT as collateral and request a loan against it" + }, + "setFeeTo(address)": { + "notice": "Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function." + }, + "takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)": { + "notice": "Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party." + }, + "transferOwnership(address,bool,bool)": { + "notice": "Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`." + }, + "withdrawFees()": { + "notice": "Withdraws the fees accumulated." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 674, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "owner", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 676, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "pendingOwner", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 12042, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "feeTo", + "offset": 0, + "slot": "2", + "type": "t_address" + }, + { + "astId": 12044, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "collateral", + "offset": 0, + "slot": "3", + "type": "t_contract(IERC721)17337" + }, + { + "astId": 12046, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "asset", + "offset": 0, + "slot": "4", + "type": "t_contract(IERC20)1405" + }, + { + "astId": 12048, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "feesEarnedShare", + "offset": 0, + "slot": "5", + "type": "t_uint256" + }, + { + "astId": 12052, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "tokenLoanParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)" + }, + { + "astId": 12074, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "tokenLoan", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)" + }, + { + "astId": 12099, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "nonces", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_address,t_uint256)" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_contract(IERC20)1405": { + "encoding": "inplace", + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IERC721)17337": { + "encoding": "inplace", + "label": "contract IERC721", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct NFTPair.TokenLoan)", + "numberOfBytes": "32", + "value": "t_struct(TokenLoan)12070_storage" + }, + "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)": { + "encoding": "mapping", + "key": "t_uint256", + "label": "mapping(uint256 => struct TokenLoanParams)", + "numberOfBytes": "32", + "value": "t_struct(TokenLoanParams)11920_storage" + }, + "t_struct(TokenLoan)12070_storage": { + "encoding": "inplace", + "label": "struct NFTPair.TokenLoan", + "members": [ + { + "astId": 12063, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "borrower", + "offset": 0, + "slot": "0", + "type": "t_address" + }, + { + "astId": 12065, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "lender", + "offset": 0, + "slot": "1", + "type": "t_address" + }, + { + "astId": 12067, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "startTime", + "offset": 20, + "slot": "1", + "type": "t_uint64" + }, + { + "astId": 12069, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "status", + "offset": 28, + "slot": "1", + "type": "t_uint8" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TokenLoanParams)11920_storage": { + "encoding": "inplace", + "label": "struct TokenLoanParams", + "members": [ + { + "astId": 11915, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "valuation", + "offset": 0, + "slot": "0", + "type": "t_uint128" + }, + { + "astId": 11917, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "duration", + "offset": 16, + "slot": "0", + "type": "t_uint64" + }, + { + "astId": 11919, + "contract": "contracts/NFTPair.sol:NFTPair", + "label": "annualInterestBPS", + "offset": 24, + "slot": "0", + "type": "t_uint16" + } + ], + "numberOfBytes": "32" + }, + "t_uint128": { + "encoding": "inplace", + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint16": { + "encoding": "inplace", + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "encoding": "inplace", + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/WETH9Mock.json b/deployments/ropsten/WETH9Mock.json new file mode 100644 index 00000000..b0e94091 --- /dev/null +++ b/deployments/ropsten/WETH9Mock.json @@ -0,0 +1,396 @@ +{ + "address": "0xf34200FB29d202867DbE147696e6c5650F81Fbb3", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x3270443f8518ec136b45052ba2716d01bd3e2622b4379b59204bede80ca3b44c", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0xf34200FB29d202867DbE147696e6c5650F81Fbb3", + "transactionIndex": 26, + "gasUsed": "541800", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0xe2289cf90a3843a161e1629499e1ff2cd2a25fcdf2746c968cbc60bcab957bfa", + "transactionHash": "0x3270443f8518ec136b45052ba2716d01bd3e2622b4379b59204bede80ca3b44c", + "logs": [], + "blockNumber": 12212436, + "cumulativeGasUsed": "5427715", + "status": 1, + "byzantium": true + }, + "args": [], + "solcInputHash": "6c88ba2f34cfbd7cd92d738b883e2e91", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"src\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"guy\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"dst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"src\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"dst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"src\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"Withdrawal\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"guy\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"decimals\",\"outputs\":[{\"internalType\":\"uint8\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"symbol\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"dst\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"src\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"dst\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"wad\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/WETH9Mock.sol\":\"WETH9Mock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"contracts/mocks/WETH9Mock.sol\":{\"content\":\"// SPDX-License-Identifier: GPL-3.0-only\\npragma solidity 0.6.12;\\n\\ncontract WETH9Mock {\\n string public name = \\\"Wrapped Ether\\\";\\n string public symbol = \\\"WETH\\\";\\n uint8 public decimals = 18;\\n\\n event Approval(address indexed src, address indexed guy, uint256 wad);\\n event Transfer(address indexed src, address indexed dst, uint256 wad);\\n event Deposit(address indexed dst, uint256 wad);\\n event Withdrawal(address indexed src, uint256 wad);\\n\\n mapping(address => uint256) public balanceOf;\\n mapping(address => mapping(address => uint256)) public allowance;\\n\\n /*fallback () external payable {\\n deposit();\\n }*/\\n function deposit() public payable {\\n balanceOf[msg.sender] += msg.value;\\n emit Deposit(msg.sender, msg.value);\\n }\\n\\n function withdraw(uint256 wad) public {\\n require(balanceOf[msg.sender] >= wad, \\\"WETH9: Error\\\");\\n balanceOf[msg.sender] -= wad;\\n msg.sender.transfer(wad);\\n emit Withdrawal(msg.sender, wad);\\n }\\n\\n function totalSupply() public view returns (uint256) {\\n return address(this).balance;\\n }\\n\\n function approve(address guy, uint256 wad) public returns (bool) {\\n allowance[msg.sender][guy] = wad;\\n emit Approval(msg.sender, guy, wad);\\n return true;\\n }\\n\\n function transfer(address dst, uint256 wad) public returns (bool) {\\n return transferFrom(msg.sender, dst, wad);\\n }\\n\\n function transferFrom(\\n address src,\\n address dst,\\n uint256 wad\\n ) public returns (bool) {\\n require(balanceOf[src] >= wad, \\\"WETH9: Error\\\");\\n\\n if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) {\\n require(allowance[src][msg.sender] >= wad, \\\"WETH9: Error\\\");\\n allowance[src][msg.sender] -= wad;\\n }\\n\\n balanceOf[src] -= wad;\\n balanceOf[dst] += wad;\\n\\n emit Transfer(src, dst, wad);\\n\\n return true;\\n }\\n}\\n\",\"keccak256\":\"0xf9a68c4ac83c020999f84bbda06e9d6f046d0979f359c92b316cf0f1bd0eb2ee\",\"license\":\"GPL-3.0-only\"}},\"version\":1}", + "bytecode": "0x60c0604052600d60808190526c2bb930b83832b21022ba3432b960991b60a090815261002e916000919061007a565b50604080518082019091526004808252630ae8aa8960e31b602090920191825261005a9160019161007a565b506002805460ff1916601217905534801561007457600080fd5b5061010d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100bb57805160ff19168380011785556100e8565b828001600101855582156100e8579182015b828111156100e85782518255916020019190600101906100cd565b506100f49291506100f8565b5090565b5b808211156100f457600081556001016100f9565b61078e8061011c6000396000f3fe60806040526004361061009c5760003560e01c8063313ce56711610064578063313ce5671461020e57806370a082311461023957806395d89b411461026c578063a9059cbb14610281578063d0e30db0146102ba578063dd62ed3e146102c25761009c565b806306fdde03146100a1578063095ea7b31461012b57806318160ddd1461017857806323b872dd1461019f5780632e1a7d4d146101e2575b600080fd5b3480156100ad57600080fd5b506100b66102fd565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100f05781810151838201526020016100d8565b50505050905090810190601f16801561011d5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561013757600080fd5b506101646004803603604081101561014e57600080fd5b506001600160a01b03813516906020013561038b565b604080519115158252519081900360200190f35b34801561018457600080fd5b5061018d6103f1565b60408051918252519081900360200190f35b3480156101ab57600080fd5b50610164600480360360608110156101c257600080fd5b506001600160a01b038135811691602081013590911690604001356103f5565b3480156101ee57600080fd5b5061020c6004803603602081101561020557600080fd5b5035610597565b005b34801561021a57600080fd5b50610223610663565b6040805160ff9092168252519081900360200190f35b34801561024557600080fd5b5061018d6004803603602081101561025c57600080fd5b50356001600160a01b031661066c565b34801561027857600080fd5b506100b661067e565b34801561028d57600080fd5b50610164600480360360408110156102a457600080fd5b506001600160a01b0381351690602001356106d8565b61020c6106ec565b3480156102ce57600080fd5b5061018d600480360360408110156102e557600080fd5b506001600160a01b038135811691602001351661073b565b6000805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103835780601f1061035857610100808354040283529160200191610383565b820191906000526020600020905b81548152906001019060200180831161036657829003601f168201915b505050505081565b3360008181526004602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b6001600160a01b038316600090815260036020526040812054821115610451576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b6001600160a01b038416331480159061048f57506001600160a01b038416600090815260046020908152604080832033845290915290205460001914155b15610526576001600160a01b03841660009081526004602090815260408083203384529091529020548211156104fb576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b6001600160a01b03841660009081526004602090815260408083203384529091529020805483900390555b6001600160a01b03808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b336000908152600360205260409020548111156105ea576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f19350505050158015610629573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103835780601f1061035857610100808354040283529160200191610383565b60006106e53384846103f5565b9392505050565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b60046020908152600092835260408084209091529082529020548156fea26469706673582212204139068e00724781f851709622caa375542ff067553b7ad1cf9805bc71cc8a5364736f6c634300060c0033", + "deployedBytecode": "0x60806040526004361061009c5760003560e01c8063313ce56711610064578063313ce5671461020e57806370a082311461023957806395d89b411461026c578063a9059cbb14610281578063d0e30db0146102ba578063dd62ed3e146102c25761009c565b806306fdde03146100a1578063095ea7b31461012b57806318160ddd1461017857806323b872dd1461019f5780632e1a7d4d146101e2575b600080fd5b3480156100ad57600080fd5b506100b66102fd565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100f05781810151838201526020016100d8565b50505050905090810190601f16801561011d5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561013757600080fd5b506101646004803603604081101561014e57600080fd5b506001600160a01b03813516906020013561038b565b604080519115158252519081900360200190f35b34801561018457600080fd5b5061018d6103f1565b60408051918252519081900360200190f35b3480156101ab57600080fd5b50610164600480360360608110156101c257600080fd5b506001600160a01b038135811691602081013590911690604001356103f5565b3480156101ee57600080fd5b5061020c6004803603602081101561020557600080fd5b5035610597565b005b34801561021a57600080fd5b50610223610663565b6040805160ff9092168252519081900360200190f35b34801561024557600080fd5b5061018d6004803603602081101561025c57600080fd5b50356001600160a01b031661066c565b34801561027857600080fd5b506100b661067e565b34801561028d57600080fd5b50610164600480360360408110156102a457600080fd5b506001600160a01b0381351690602001356106d8565b61020c6106ec565b3480156102ce57600080fd5b5061018d600480360360408110156102e557600080fd5b506001600160a01b038135811691602001351661073b565b6000805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103835780601f1061035857610100808354040283529160200191610383565b820191906000526020600020905b81548152906001019060200180831161036657829003601f168201915b505050505081565b3360008181526004602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a350600192915050565b4790565b6001600160a01b038316600090815260036020526040812054821115610451576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b6001600160a01b038416331480159061048f57506001600160a01b038416600090815260046020908152604080832033845290915290205460001914155b15610526576001600160a01b03841660009081526004602090815260408083203384529091529020548211156104fb576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b6001600160a01b03841660009081526004602090815260408083203384529091529020805483900390555b6001600160a01b03808516600081815260036020908152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a35060019392505050565b336000908152600360205260409020548111156105ea576040805162461bcd60e51b815260206004820152600c60248201526b2ba2aa241c9d1022b93937b960a11b604482015290519081900360640190fd5b33600081815260036020526040808220805485900390555183156108fc0291849190818181858888f19350505050158015610629573d6000803e3d6000fd5b5060408051828152905133917f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65919081900360200190a250565b60025460ff1681565b60036020526000908152604090205481565b60018054604080516020600284861615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103835780601f1061035857610100808354040283529160200191610383565b60006106e53384846103f5565b9392505050565b33600081815260036020908152604091829020805434908101909155825190815291517fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c9281900390910190a2565b60046020908152600092835260408084209091529082529020548156fea26469706673582212204139068e00724781f851709622caa375542ff067553b7ad1cf9805bc71cc8a5364736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": {}, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": {}, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 4, + "contract": "contracts/mocks/WETH9Mock.sol:WETH9Mock", + "label": "name", + "offset": 0, + "slot": "0", + "type": "t_string_storage" + }, + { + "astId": 7, + "contract": "contracts/mocks/WETH9Mock.sol:WETH9Mock", + "label": "symbol", + "offset": 0, + "slot": "1", + "type": "t_string_storage" + }, + { + "astId": 10, + "contract": "contracts/mocks/WETH9Mock.sol:WETH9Mock", + "label": "decimals", + "offset": 0, + "slot": "2", + "type": "t_uint8" + }, + { + "astId": 42, + "contract": "contracts/mocks/WETH9Mock.sol:WETH9Mock", + "label": "balanceOf", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 48, + "contract": "contracts/mocks/WETH9Mock.sol:WETH9Mock", + "label": "allowance", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_string_storage": { + "encoding": "bytes", + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "encoding": "inplace", + "label": "uint8", + "numberOfBytes": "1" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/solcInputs/3285d4ce4a1fc523877d40dd9fd97bbb.json b/deployments/ropsten/solcInputs/3285d4ce4a1fc523877d40dd9fd97bbb.json new file mode 100644 index 00000000..a6a4124b --- /dev/null +++ b/deployments/ropsten/solcInputs/3285d4ce4a1fc523877d40dd9fd97bbb.json @@ -0,0 +1,158 @@ +{ + "language": "Solidity", + "sources": { + "contracts/Cauldron.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract Cauldron is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n Cauldron public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 private constant INTEREST_PER_SECOND = 317097920;\r\n\r\n uint256 private constant COLLATERIZATION_RATE = 75000; // 75%\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 private constant LIQUIDATION_MULTIPLIER = 112000; // add 12%\r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 private constant BORROW_OPENING_FEE = 50; // 0.05%\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData) = abi.decode(data, (IERC20, IOracle, bytes));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\r\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\r\n }\r\n}\r\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\n/// @notice A library for performing overflow-/underflow-safe math,\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\nlibrary BoringMath {\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require(b == 0 || (c = a * b) / b == a, \"BoringMath: Mul Overflow\");\n }\n\n function to128(uint256 a) internal pure returns (uint128 c) {\n require(a <= uint128(-1), \"BoringMath: uint128 Overflow\");\n c = uint128(a);\n }\n\n function to64(uint256 a) internal pure returns (uint64 c) {\n require(a <= uint64(-1), \"BoringMath: uint64 Overflow\");\n c = uint64(a);\n }\n\n function to32(uint256 a) internal pure returns (uint32 c) {\n require(a <= uint32(-1), \"BoringMath: uint32 Overflow\");\n c = uint32(a);\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\nlibrary BoringMath128 {\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\nlibrary BoringMath64 {\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\nlibrary BoringMath32 {\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\n// Edited by BoringCrypto\n\ncontract BoringOwnableData {\n address public owner;\n address public pendingOwner;\n}\n\ncontract BoringOwnable is BoringOwnableData {\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /// @notice `owner` defaults to msg.sender on construction.\n constructor() public {\n owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\n /// Can only be invoked by the current `owner`.\n /// @param newOwner Address of the new owner.\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\n function transferOwnership(\n address newOwner,\n bool direct,\n bool renounce\n ) public onlyOwner {\n if (direct) {\n // Checks\n require(newOwner != address(0) || renounce, \"Ownable: zero address\");\n\n // Effects\n emit OwnershipTransferred(owner, newOwner);\n owner = newOwner;\n pendingOwner = address(0);\n } else {\n // Effects\n pendingOwner = newOwner;\n }\n }\n\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\n function claimOwnership() public {\n address _pendingOwner = pendingOwner;\n\n // Checks\n require(msg.sender == _pendingOwner, \"Ownable: caller != pending owner\");\n\n // Effects\n emit OwnershipTransferred(owner, _pendingOwner);\n owner = _pendingOwner;\n pendingOwner = address(0);\n }\n\n /// @notice Only allows the `owner` to execute the function.\n modifier onlyOwner() {\n require(msg.sender == owner, \"Ownable: caller is not the owner\");\n _;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/ERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"./interfaces/IERC20.sol\";\nimport \"./Domain.sol\";\n\n// solhint-disable no-inline-assembly\n// solhint-disable not-rely-on-time\n\n// Data part taken out for building of contracts that receive delegate calls\ncontract ERC20Data {\n /// @notice owner > balance mapping.\n mapping(address => uint256) public balanceOf;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n}\n\nabstract contract ERC20 is IERC20, Domain {\n /// @notice owner > balance mapping.\n mapping(address => uint256) public override balanceOf;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public override allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n\n /// @notice Transfers `amount` tokens from `msg.sender` to `to`.\n /// @param to The address to move the tokens.\n /// @param amount of the tokens to move.\n /// @return (bool) Returns True if succeeded.\n function transfer(address to, uint256 amount) public returns (bool) {\n // If `amount` is 0, or `msg.sender` is `to` nothing happens\n if (amount != 0 || msg.sender == to) {\n uint256 srcBalance = balanceOf[msg.sender];\n require(srcBalance >= amount, \"ERC20: balance too low\");\n if (msg.sender != to) {\n require(to != address(0), \"ERC20: no zero address\"); // Moved down so low balance calls safe some gas\n\n balanceOf[msg.sender] = srcBalance - amount; // Underflow is checked\n balanceOf[to] += amount;\n }\n }\n emit Transfer(msg.sender, to, amount);\n return true;\n }\n\n /// @notice Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\n /// @param from Address to draw tokens from.\n /// @param to The address to move the tokens.\n /// @param amount The token amount to move.\n /// @return (bool) Returns True if succeeded.\n function transferFrom(\n address from,\n address to,\n uint256 amount\n ) public returns (bool) {\n // If `amount` is 0, or `from` is `to` nothing happens\n if (amount != 0) {\n uint256 srcBalance = balanceOf[from];\n require(srcBalance >= amount, \"ERC20: balance too low\");\n\n if (from != to) {\n uint256 spenderAllowance = allowance[from][msg.sender];\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\n if (spenderAllowance != type(uint256).max) {\n require(spenderAllowance >= amount, \"ERC20: allowance too low\");\n allowance[from][msg.sender] = spenderAllowance - amount; // Underflow is checked\n }\n require(to != address(0), \"ERC20: no zero address\"); // Moved down so other failed calls safe some gas\n\n balanceOf[from] = srcBalance - amount; // Underflow is checked\n balanceOf[to] += amount;\n }\n }\n emit Transfer(from, to, amount);\n return true;\n }\n\n /// @notice Approves `amount` from sender to be spend by `spender`.\n /// @param spender Address of the party that can draw from msg.sender's account.\n /// @param amount The maximum collective amount that `spender` can draw.\n /// @return (bool) Returns True if approved.\n function approve(address spender, uint256 amount) public override returns (bool) {\n allowance[msg.sender][spender] = amount;\n emit Approval(msg.sender, spender, amount);\n return true;\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\n\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\n /// @param owner_ Address of the owner.\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\n /// @param value The maximum collective amount that `spender` can draw.\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override {\n require(owner_ != address(0), \"ERC20: Owner cannot be 0\");\n require(block.timestamp < deadline, \"ERC20: Expired\");\n require(\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\n owner_,\n \"ERC20: Invalid Signature\"\n );\n allowance[owner_][spender] = value;\n emit Approval(owner_, spender, value);\n }\n}\n\ncontract ERC20WithSupply is IERC20, ERC20 {\n uint256 public override totalSupply;\n\n function _mint(address user, uint256 amount) internal {\n uint256 newTotalSupply = totalSupply + amount;\n require(newTotalSupply >= totalSupply, \"Mint overflow\");\n totalSupply = newTotalSupply;\n balanceOf[user] += amount;\n emit Transfer(address(0), user, amount);\n }\n\n function _burn(address user, uint256 amount) internal {\n require(balanceOf[user] >= amount, \"Burn too much\");\n totalSupply -= amount;\n balanceOf[user] -= amount;\n emit Transfer(user, address(0), amount);\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IMasterContract {\n /// @notice Init function that gets called from `BoringFactory.deploy`.\n /// Also kown as the constructor for cloned contracts.\n /// Any ETH send to `BoringFactory.deploy` ends up here.\n /// @param data Can be abi encoded arguments or anything else.\n function init(bytes calldata data) external payable;\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"./BoringMath.sol\";\n\nstruct Rebase {\n uint128 elastic;\n uint128 base;\n}\n\n/// @notice A rebasing library using overflow-/underflow-safe math.\nlibrary RebaseLibrary {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\n function toBase(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (uint256 base) {\n if (total.elastic == 0) {\n base = elastic;\n } else {\n base = elastic.mul(total.base) / total.elastic;\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\n base = base.add(1);\n }\n }\n }\n\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\n function toElastic(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (uint256 elastic) {\n if (total.base == 0) {\n elastic = base;\n } else {\n elastic = base.mul(total.elastic) / total.base;\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\n elastic = elastic.add(1);\n }\n }\n }\n\n /// @notice Add `elastic` to `total` and doubles `total.base`.\n /// @return (Rebase) The new total.\n /// @return base in relationship to `elastic`.\n function add(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 base) {\n base = toBase(total, elastic, roundUp);\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return (total, base);\n }\n\n /// @notice Sub `base` from `total` and update `total.elastic`.\n /// @return (Rebase) The new total.\n /// @return elastic in relationship to `base`.\n function sub(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 elastic) {\n elastic = toElastic(total, base, roundUp);\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return (total, elastic);\n }\n\n /// @notice Add `elastic` and `base` to `total`.\n function add(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return total;\n }\n\n /// @notice Subtract `elastic` and `base` to `total`.\n function sub(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return total;\n }\n\n /// @notice Add `elastic` to `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.add(elastic.to128());\n }\n\n /// @notice Subtract `elastic` from `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"../interfaces/IERC20.sol\";\n\n// solhint-disable avoid-low-level-calls\n\nlibrary BoringERC20 {\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\n\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\n if (data.length >= 64) {\n return abi.decode(data, (string));\n } else if (data.length == 32) {\n uint8 i = 0;\n while (i < 32 && data[i] != 0) {\n i++;\n }\n bytes memory bytesArray = new bytes(i);\n for (i = 0; i < 32 && data[i] != 0; i++) {\n bytesArray[i] = data[i];\n }\n return string(bytesArray);\n } else {\n return \"???\";\n }\n }\n\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\n /// @param token The address of the ERC-20 token contract.\n /// @return (string) Token symbol.\n function safeSymbol(IERC20 token) internal view returns (string memory) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\n return success ? returnDataToString(data) : \"???\";\n }\n\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\n /// @param token The address of the ERC-20 token contract.\n /// @return (string) Token name.\n function safeName(IERC20 token) internal view returns (string memory) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\n return success ? returnDataToString(data) : \"???\";\n }\n\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\n /// @param token The address of the ERC-20 token contract.\n /// @return (uint8) Token decimals.\n function safeDecimals(IERC20 token) internal view returns (uint8) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\n }\n\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\n /// @param token The address of the ERC-20 token.\n /// @param to The address of the user to check.\n /// @return amount The token amount.\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\n require(success && data.length >= 32, \"BoringERC20: BalanceOf failed\");\n amount = abi.decode(data, (uint256));\n }\n\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransfer(\n IERC20 token,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: Transfer failed\");\n }\n\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param from Transfer tokens from.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransferFrom(\n IERC20 token,\n address from,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: TransferFrom failed\");\n }\n}\n" + }, + "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\nimport './IBatchFlashBorrower.sol';\nimport './IFlashBorrower.sol';\nimport './IStrategy.sol';\n\ninterface IBentoBoxV1 {\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\n event LogRegisterProtocol(address indexed protocol);\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\n event LogStrategyDivest(address indexed token, uint256 amount);\n event LogStrategyInvest(address indexed token, uint256 amount);\n event LogStrategyLoss(address indexed token, uint256 amount);\n event LogStrategyProfit(address indexed token, uint256 amount);\n event LogStrategyQueued(address indexed token, address indexed strategy);\n event LogStrategySet(address indexed token, address indexed strategy);\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n function balanceOf(IERC20, address) external view returns (uint256);\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\n function claimOwnership() external;\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\n function masterContractApproved(address, address) external view returns (bool);\n function masterContractOf(address) external view returns (address);\n function nonces(address) external view returns (uint256);\n function owner() external view returns (address);\n function pendingOwner() external view returns (address);\n function pendingStrategy(IERC20) external view returns (IStrategy);\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\n function registerProtocol() external;\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\n function strategy(IERC20) external view returns (IStrategy);\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\n function totals(IERC20) external view returns (Rebase memory totals_);\n function transfer(IERC20 token, address from, address to, uint256 share) external;\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\n function whitelistMasterContract(address masterContract, bool approved) external;\n function whitelistedMasterContracts(address) external view returns (bool);\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\n}" + }, + "contracts/MagicInternetMoney.sol": { + "content": "// SPDX-License-Identifier: MIT\r\n\r\n// Magic Internet Money\r\n\r\n// ███╗ ███╗██╗███╗ ███╗\r\n// ████╗ ████║██║████╗ ████║\r\n// ██╔████╔██║██║██╔████╔██║\r\n// ██║╚██╔╝██║██║██║╚██╔╝██║\r\n// ██║ ╚═╝ ██║██║██║ ╚═╝ ██║\r\n// ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝\r\n\r\n// BoringCrypto, 0xMerlin\r\n\r\npragma solidity 0.6.12;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract MagicInternetMoney is ERC20, BoringOwnable {\r\n using BoringMath for uint256;\r\n // ERC20 'variables'\r\n string public constant symbol = \"MIM\";\r\n string public constant name = \"Magic Internet Money\";\r\n uint8 public constant decimals = 18;\r\n uint256 public override totalSupply;\r\n\r\n struct Minting {\r\n uint128 time;\r\n uint128 amount;\r\n }\r\n\r\n Minting public lastMint;\r\n uint256 private constant MINTING_PERIOD = 24 hours;\r\n uint256 private constant MINTING_INCREASE = 15000;\r\n uint256 private constant MINTING_PRECISION = 1e5;\r\n\r\n function mint(address to, uint256 amount) public onlyOwner {\r\n require(to != address(0), \"MIM: no mint to zero address\");\r\n\r\n // Limits the amount minted per period to a convergence function, with the period duration restarting on every mint\r\n uint256 totalMintedAmount = uint256(lastMint.time < block.timestamp - MINTING_PERIOD ? 0 : lastMint.amount).add(amount);\r\n require(totalSupply == 0 || totalSupply.mul(MINTING_INCREASE) / MINTING_PRECISION >= totalMintedAmount);\r\n\r\n lastMint.time = block.timestamp.to128();\r\n lastMint.amount = totalMintedAmount.to128();\r\n\r\n totalSupply = totalSupply + amount;\r\n balanceOf[to] += amount;\r\n emit Transfer(address(0), to, amount);\r\n }\r\n\r\n function mintToBentoBox(address clone, uint256 amount, IBentoBoxV1 bentoBox) public onlyOwner {\r\n mint(address(bentoBox), amount);\r\n bentoBox.deposit(IERC20(address(this)), address(bentoBox), clone, amount, 0);\r\n }\r\n\r\n function burn(uint256 amount) public {\r\n require(amount <= balanceOf[msg.sender], \"MIM: not enough\");\r\n\r\n balanceOf[msg.sender] -= amount;\r\n totalSupply -= amount;\r\n emit Transfer(msg.sender, address(0), amount);\r\n }\r\n}\r\n" + }, + "contracts/interfaces/IOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\n\ninterface IOracle {\n /// @notice Get the latest exchange rate.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return success if no valid (recent) rate is available, return false else true.\n /// @return rate The rate of the requested asset / pair / pool.\n function get(bytes calldata data) external returns (bool success, uint256 rate);\n\n /// @notice Check the last exchange rate without any state changes.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return success if no valid (recent) rate is available, return false else true.\n /// @return rate The rate of the requested asset / pair / pool.\n function peek(bytes calldata data) external view returns (bool success, uint256 rate);\n\n /// @notice Check the current spot exchange rate without any state changes. For oracles like TWAP this will be different from peek().\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return rate The rate of the requested asset / pair / pool.\n function peekSpot(bytes calldata data) external view returns (uint256 rate);\n\n /// @notice Returns a human readable (short) name about this oracle.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return (string) A human readable symbol name about this oracle.\n function symbol(bytes calldata data) external view returns (string memory);\n\n /// @notice Returns a human readable name about this oracle.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return (string) A human readable name about this oracle.\n function name(bytes calldata data) external view returns (string memory);\n}\n" + }, + "contracts/interfaces/ISwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\n\ninterface ISwapper {\n /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for at least 'amountToMin' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Returns the amount of tokens 'to' transferred to BentoBox.\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) external returns (uint256 extraShare, uint256 shareReturned);\n\n /// @notice Calculates the amount of token 'from' needed to complete the swap (amountFrom),\n /// this should be less than or equal to amountFromMax.\n /// Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for exactly 'exactAmountTo' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Transfers allocated, but unused 'from' tokens within the BentoBox to 'refundTo' (amountFromMax - amountFrom).\n /// Returns the amount of 'from' tokens withdrawn from BentoBox (amountFrom).\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) external returns (uint256 shareUsed, uint256 shareReturned);\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IERC20 {\n function totalSupply() external view returns (uint256);\n\n function balanceOf(address account) external view returns (uint256);\n\n function allowance(address owner, address spender) external view returns (uint256);\n\n function approve(address spender, uint256 amount) external returns (bool);\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /// @notice EIP 2612\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/Domain.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Based on code and smartness by Ross Campbell and Keno\n// Uses immutable to store the domain separator to reduce gas usage\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\npragma solidity 0.6.12;\n\n// solhint-disable no-inline-assembly\n\ncontract Domain {\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\"EIP712Domain(uint256 chainId,address verifyingContract)\");\n // See https://eips.ethereum.org/EIPS/eip-191\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \"\\x19\\x01\";\n\n // solhint-disable var-name-mixedcase\n bytes32 private immutable _DOMAIN_SEPARATOR;\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\n\n /// @dev Calculate the DOMAIN_SEPARATOR\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\n }\n\n constructor() public {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\n }\n\n /// @dev Return the DOMAIN_SEPARATOR\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\n // solhint-disable-next-line func-name-mixedcase\n function _domainSeparator() internal view returns (bytes32) {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\n }\n\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\n }\n}\n" + }, + "@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\n\ninterface IBatchFlashBorrower {\n function onBatchFlashLoan(\n address sender,\n IERC20[] calldata tokens,\n uint256[] calldata amounts,\n uint256[] calldata fees,\n bytes calldata data\n ) external;\n}" + }, + "@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\n\ninterface IFlashBorrower {\n function onFlashLoan(\n address sender,\n IERC20 token,\n uint256 amount,\n uint256 fee,\n bytes calldata data\n ) external;\n}" + }, + "@sushiswap/bentobox-sdk/contracts/IStrategy.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IStrategy {\n // Send the assets to the Strategy and call skim to invest them\n function skim(uint256 amount) external;\n\n // Harvest any profits made converted to the asset and pass them to the caller\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\n\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\n\n // Withdraw all assets in the safest way possible. This shouldn't fail.\n function exit(uint256 balance) external returns (int256 amountAdded);\n}" + }, + "contracts/interfaces/IKashiPair.sol": { + "content": "// SPDX-License-Identifier: MIT\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./IOracle.sol\";\nimport \"./ISwapper.sol\";\n\ninterface IKashiPair {\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n event LogAccrue(uint256 accruedAmount, uint256 feeFraction, uint64 rate, uint256 utilization);\n event LogAddAsset(address indexed from, address indexed to, uint256 share, uint256 fraction);\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogExchangeRate(uint256 rate);\n event LogFeeTo(address indexed newFeeTo);\n event LogRemoveAsset(address indexed from, address indexed to, uint256 share, uint256 fraction);\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n\n function DOMAIN_SEPARATOR() external view returns (bytes32);\n\n function accrue() external;\n\n function accrueInfo()\n external\n view\n returns (\n uint64 interestPerBlock,\n uint64 lastBlockAccrued,\n uint128 feesEarnedFraction\n );\n\n function addAsset(\n address to,\n bool skim,\n uint256 share\n ) external returns (uint256 fraction);\n\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) external;\n\n function allowance(address, address) external view returns (uint256);\n\n function approve(address spender, uint256 amount) external returns (bool);\n\n function asset() external view returns (IERC20);\n\n function balanceOf(address) external view returns (uint256);\n\n function bentoBox() external view returns (IBentoBoxV1);\n\n function borrow(address to, uint256 amount) external returns (uint256 part, uint256 share);\n\n function claimOwnership() external;\n\n function collateral() external view returns (IERC20);\n\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2);\n\n function decimals() external view returns (uint8);\n\n function exchangeRate() external view returns (uint256);\n\n function feeTo() external view returns (address);\n\n function getInitData(\n IERC20 collateral_,\n IERC20 asset_,\n IOracle oracle_,\n bytes calldata oracleData_\n ) external pure returns (bytes memory data);\n\n function init(bytes calldata data) external payable;\n\n function isSolvent(address user, bool open) external view returns (bool);\n\n function liquidate(\n address[] calldata users,\n uint256[] calldata borrowParts,\n address to,\n ISwapper swapper,\n bool open\n ) external;\n\n function masterContract() external view returns (address);\n\n function name() external view returns (string memory);\n\n function nonces(address) external view returns (uint256);\n\n function oracle() external view returns (IOracle);\n\n function oracleData() external view returns (bytes memory);\n\n function owner() external view returns (address);\n\n function pendingOwner() external view returns (address);\n\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n function removeAsset(address to, uint256 fraction) external returns (uint256 share);\n\n function removeCollateral(address to, uint256 share) external;\n\n function repay(\n address to,\n bool skim,\n uint256 part\n ) external returns (uint256 amount);\n\n function setFeeTo(address newFeeTo) external;\n\n function setSwapper(ISwapper swapper, bool enable) external;\n\n function swappers(ISwapper) external view returns (bool);\n\n function symbol() external view returns (string memory);\n\n function totalAsset() external view returns (uint128 elastic, uint128 base);\n\n function totalBorrow() external view returns (uint128 elastic, uint128 base);\n\n function totalCollateralShare() external view returns (uint256);\n\n function totalSupply() external view returns (uint256);\n\n function transfer(address to, uint256 amount) external returns (bool);\n\n function transferFrom(\n address from,\n address to,\n uint256 amount\n ) external returns (bool);\n\n function transferOwnership(\n address newOwner,\n bool direct,\n bool renounce\n ) external;\n\n function updateExchangeRate() external returns (bool updated, uint256 rate);\n\n function userBorrowPart(address) external view returns (uint256);\n\n function userCollateralShare(address) external view returns (uint256);\n\n function withdrawFees() external;\n}\n" + }, + "contracts/PrivatePool.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Cauldron\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./interfaces/IOracle.sol\";\nimport \"./interfaces/ISimpleSwapper.sol\";\nimport \"./libraries/FullMath.sol\";\n\n/// @title PrivatePool\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract PrivatePool is BoringOwnable, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogExchangeRate(uint256 rate);\n event LogAccrue(uint256 accruedAmount, uint256 feeAmount);\n event LogAddCollateral(\n address indexed from,\n address indexed to,\n uint256 share\n );\n event LogAddAsset(address indexed from, uint256 share);\n event LogRemoveCollateral(\n address indexed from,\n address indexed to,\n uint256 share\n );\n event LogRemoveAsset(address indexed to, uint256 share);\n event LogBorrow(\n address indexed from,\n address indexed to,\n uint256 amount,\n uint256 openFeeAmount,\n uint256 part\n );\n event LogRepay(\n address indexed from,\n address indexed to,\n uint256 amount,\n uint256 part\n );\n event LogSeizeCollateral(\n address indexed from,\n uint256 collateralShare,\n uint256 debtAmount,\n uint256 debtPart\n );\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(\n address indexed feeTo,\n uint256 assetFeeShare,\n uint256 collateralFeeShare\n );\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n PrivatePool public immutable masterContract;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC20 public collateral;\n IERC20 public asset;\n IOracle public oracle;\n bytes public oracleData;\n\n // A note on terminology:\n // \"Shares\" are BentoBox shares.\n // \"Parts\" and represent shares held in the debt pool\n\n // The BentoBox balance is the sum of the below two.\n // Since that fits in a single uint128, we can often forgo overflow checks.\n struct AssetBalance {\n uint128 reservesShare;\n uint128 feesEarnedShare;\n }\n AssetBalance public assetBalance;\n uint256 public feesOwedAmount; // Positive only if reservesShare = 0\n\n // The BentoBox balance is the sum of the below two.\n // Seized collateral goes to the \"userCollateralShare\" account of the\n // lender.\n struct CollateralBalance {\n uint128 userTotalShare;\n uint128 feesEarnedShare;\n }\n CollateralBalance public collateralBalance;\n mapping(address => uint256) public userCollateralShare;\n\n // Elastic: Exact asset token amount that currently needs to be repaid\n // Base: Total parts of the debt held by borrowers (borrowerDebtPart)\n Rebase public totalDebt;\n mapping(address => uint256) public borrowerDebtPart;\n\n address public lender;\n mapping(address => bool) public approvedBorrowers;\n\n /// @notice Exchange and interest rate tracking.\n /// This is 'cached' here because calls to Oracles can be very expensive.\n uint256 public exchangeRate;\n\n struct AccrueInfo {\n uint64 lastAccrued;\n uint64 INTEREST_PER_SECOND; // (in units of 1/10^18)\n uint64 EXPIRATION;\n uint16 COLLATERALIZATION_RATE_BPS;\n uint16 LIQUIDATION_MULTIPLIER_BPS;\n uint16 BORROW_OPENING_FEE_BPS;\n bool LIQUIDATION_SEIZE_COLLATERAL;\n }\n AccrueInfo public accrueInfo;\n\n uint256 private constant PROTOCOL_FEE_BPS = 1000; // 10%\n uint256 private constant BPS = 10_000;\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\n\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n masterContract = this;\n }\n\n struct InitSettings {\n IERC20 collateral;\n IERC20 asset;\n IOracle oracle;\n bytes oracleData;\n address lender;\n address[] borrowers;\n uint64 INTEREST_PER_SECOND;\n uint64 EXPIRATION;\n uint16 COLLATERALIZATION_RATE_BPS;\n uint16 LIQUIDATION_MULTIPLIER_BPS;\n uint16 BORROW_OPENING_FEE_BPS;\n bool LIQUIDATION_SEIZE_COLLATERAL;\n }\n\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\n function init(bytes calldata data) public payable override {\n require(\n address(collateral) == address(0),\n \"PrivatePool: already initialized\"\n );\n\n InitSettings memory settings = abi.decode(data, (InitSettings));\n require(\n address(settings.collateral) != address(0),\n \"PrivatePool: bad pair\"\n );\n require(\n settings.LIQUIDATION_MULTIPLIER_BPS >= BPS,\n \"PrivatePool: negative liquidation bonus\"\n );\n require(\n settings.COLLATERALIZATION_RATE_BPS <= BPS,\n \"PrivatePool: bad collateralization rate\"\n );\n\n collateral = settings.collateral;\n asset = settings.asset;\n oracle = settings.oracle;\n oracleData = settings.oracleData;\n lender = settings.lender;\n\n AccrueInfo memory _aI;\n _aI.INTEREST_PER_SECOND = settings.INTEREST_PER_SECOND;\n _aI.EXPIRATION = settings.EXPIRATION == 0\n ? uint64(-1)\n : settings.EXPIRATION;\n _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS;\n _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS;\n _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS;\n _aI.LIQUIDATION_SEIZE_COLLATERAL = settings\n .LIQUIDATION_SEIZE_COLLATERAL;\n accrueInfo = _aI;\n\n for (uint256 i = 0; i < settings.borrowers.length; i++) {\n approvedBorrowers[settings.borrowers[i]] = true;\n }\n }\n\n function setApprovedBorrowers(address borrower, bool approved)\n external\n onlyOwner\n {\n approvedBorrowers[borrower] = approved;\n }\n\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\n function accrue() public {\n AccrueInfo memory _accrueInfo = accrueInfo;\n // Number of seconds since accrue was called\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\n if (elapsedTime == 0) {\n return;\n }\n accrueInfo.lastAccrued = uint64(block.timestamp);\n\n Rebase memory _totalDebt = totalDebt;\n if (_totalDebt.base == 0) {\n return;\n }\n // No overflow:\n // - _totalDebt.elastic is 128 bits\n // - INTEREST_PER_SECOND is 64 bits\n // - elapsedTime fits in 64 bits for the next 580 billion (10^9) years\n uint256 extraAmount = (uint256(_totalDebt.elastic) *\n _accrueInfo.INTEREST_PER_SECOND *\n elapsedTime) / 1e18;\n\n // If the interest rate is too high, then this will overflow and\n // effectively lock up all funds. Do not set the interest rate too\n // high.\n _totalDebt.elastic = _totalDebt.elastic.add(extraAmount.to128());\n totalDebt = _totalDebt;\n\n // No overflow: extraAmount is divided by 1e18 > 2^59; we need 16 bits\n uint256 feeAmount = (extraAmount * PROTOCOL_FEE_BPS) / BPS;\n\n AssetBalance memory _assetBalance = assetBalance;\n if (_assetBalance.reservesShare == 0) {\n // Fees owed are always part of the debt, and the debt just got\n // at least `feeAmount` added to it. If that fit, so does this:\n feesOwedAmount += feeAmount;\n } else {\n uint256 feeShare = bentoBox.toShare(asset, feeAmount, false);\n if (_assetBalance.reservesShare < feeShare) {\n _assetBalance.feesEarnedShare += _assetBalance.reservesShare;\n feesOwedAmount += bentoBox.toAmount(\n asset,\n feeShare - _assetBalance.reservesShare,\n false\n );\n _assetBalance.reservesShare = 0;\n } else {\n // feesEarned + fee <= feesEarned + reserves <= Bento balance:\n _assetBalance.reservesShare -= uint128(feeShare);\n _assetBalance.feesEarnedShare += uint128(feeShare);\n }\n assetBalance = _assetBalance;\n }\n\n emit LogAccrue(extraAmount, feeAmount);\n }\n\n function _isSolvent(address borrower) internal returns (bool) {\n (, uint256 _exchangeRate) = updateExchangeRate();\n\n // accrue must have already been called!\n uint256 debtPart = borrowerDebtPart[borrower];\n if (debtPart == 0) return true;\n uint256 collateralShare = userCollateralShare[borrower];\n if (collateralShare == 0) return false;\n\n // The inequality that needs to hold for a user to be solvent is:\n //\n // (value of user collateral) * (max LTV) >= (value of user debt)\n //\n // Our exchange rates give \"collateral per ether of asset\", so it makes\n // sense to express the value in the collateral token. The calculation\n // for the collateral value is:\n //\n // BentoBox tokens\n // value = shares * ---------------\n // BentoBox shares\n //\n // where BentoBox tokens resp. shares refer to the balances for the\n // collateral (ERC20) contract. For the debt we get:\n //\n // Total debt tokens Collateral wei per asset ether\n // value = parts * ----------------- * ------------------------------\n // Total debt parts 1e18\n //\n // ..since \"tokens\" is in wei. We call this EXCHANGE_RATE_PRECISION.\n // Finally, max LTV is\n //\n // COLLATERALIZATION_RATE_BPS\n // ratio = --------------------------.\n // 1e4\n //\n // We use the below table to (fit the inequality in 80 characters and)\n // move some things around and justify our calculations.\n //\n // Description Variable in contract Bits\n // -----------------------------------------------------------------\n // cs shares collateralShare 128*\n // Be BentoBox tokens bentoBoxTotals.elastic 128\n // Bb BentoBox shares bentoBoxTotals.base 128\n // -----------------------------------------------------------------\n // dp parts debtPart 128*\n // De Total debt tokens totalDebt.elastic 128\n // Db Total debt parts totalDebt.base 128\n // xr Coll. wei per asset ether exchangeRate 256\n // 1e18 EXCHANGE_RATE_PRECISION 60\n // ----------------------------------------------------------------\n // ltv COLLATERALIZATION_RATE_BPS 14\n // 1e4 BPS 14\n // 1e14 47\n //\n // (* as in other proofs, these values fit in some 128-bit total).\n // The inequality, without regard for integer division:\n //\n // Be ltv De xr\n // cs * -- * --- >= dp * -- * ----\n // Bb 1e4 Db 1e18\n //\n // This is equivalent to:\n //\n // cs * Be * ltv * Db * 1e14 >= dp * De * xr * Bb\n //\n // Corresponding bit counts:\n //\n // 128 + 128 + 14 + 128 + 47; 128 + 128 + 256 + 128\n //\n // So, the LHS definitely fits in 512 bits, and as long as one token is\n // not 2^68 times as valuable as another. Of course the other terms\n // leave some leeway, too; if the exchange rate is this high, then the\n // Bento share count is very unlikely to take up the full 128 bits.\n //\n // \"Full\" multiplication is not very expensive or cumbersome; the case\n // where `exchangeRate` is too big is a little more involved, but\n // manageable.\n\n Rebase memory _totalDebt = totalDebt;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n\n (uint256 leftLo, uint256 leftHi) = FullMath.fullMul(\n collateralShare * bentoBoxTotals.elastic,\n // Cast needed to avoid uint128 overflow\n uint256(accrueInfo.COLLATERALIZATION_RATE_BPS) *\n _totalDebt.base *\n 1e14\n );\n uint256 rightLo;\n uint256 rightHi;\n if (_exchangeRate <= type(uint128).max) {\n (rightLo, rightHi) = FullMath.fullMul(\n debtPart * _totalDebt.elastic,\n _exchangeRate * bentoBoxTotals.base\n );\n } else {\n // We multiply it out in stages to be safe. If the total overflows\n // 512 bits then we are done, as the LHS is guaranteed to be less.\n //\n // aHi * 2^256 + aLo = dp * De * Bb\n // -----------------------------------------------------------------\n // The total is then the sum of:\n //\n // bHi * 2^512 + bLo * 2^256 = xr * aHi * 2^256\n // cHi * 2^256 + cLo = xr * aLo\n //\n (uint256 aLo, uint256 aHi) = FullMath.fullMul(\n debtPart * _totalDebt.elastic,\n bentoBoxTotals.base\n );\n (uint256 bLo, uint256 bHi) = FullMath.fullMul(_exchangeRate, aHi);\n if (bHi > 0) {\n return false;\n }\n\n uint256 cHi; // (cLo is rightLo)\n (rightLo, cHi) = FullMath.fullMul(_exchangeRate, aLo);\n rightHi = cHi + bLo;\n if (rightHi < cHi) {\n return false;\n }\n }\n return leftHi > rightHi || (leftHi == rightHi && leftLo >= rightLo);\n }\n\n /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body.\n modifier solvent() {\n _;\n require(_isSolvent(msg.sender), \"PrivatePool: borrower insolvent\");\n }\n\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\n /// @return updated True if `exchangeRate` was updated.\n /// @return rate The new exchange rate.\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\n (updated, rate) = oracle.get(oracleData);\n\n if (updated) {\n exchangeRate = rate;\n emit LogExchangeRate(rate);\n } else {\n // Return the old rate if fetching wasn't successful\n rate = exchangeRate;\n }\n }\n\n /// @dev Helper function to move tokens.\n /// @param token The ERC-20 token.\n /// @param share The amount in shares to add.\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\n /// Only used for accounting checks.\n /// @param skim If True, only does a balance check on this contract.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n function _addTokens(\n IERC20 token,\n uint256 share,\n uint256 total,\n bool skim\n ) internal {\n if (skim) {\n require(\n share <= bentoBox.balanceOf(token, address(this)).sub(total),\n \"PrivatePool: skim too much\"\n );\n } else {\n bentoBox.transfer(token, msg.sender, address(this), share);\n }\n }\n\n /// @notice Adds `collateral` from msg.sender to the account `to`.\n /// @param to The receiver of the tokens.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add for `to`.\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) public {\n uint256 supplied = userCollateralShare[to];\n require(\n supplied > 0 || approvedBorrowers[to],\n \"PrivatePool: unapproved borrower\"\n );\n\n // No overflow: the total for ALL users fits in 128 bits, or the\n // BentoBox transfer (_addTokens) fails.\n userCollateralShare[to] = supplied + share;\n CollateralBalance memory _collateralBalance = collateralBalance;\n // No overflow: the sum fits in the BentoBox total\n uint256 prevTotal = _collateralBalance.userTotalShare +\n _collateralBalance.feesEarnedShare;\n // No overflow if cast safe: fits in the BentoBox or _addTokens reverts\n // Cast safe: _addTokens does not truncate the value\n collateralBalance.userTotalShare =\n _collateralBalance.userTotalShare +\n uint128(share);\n _addTokens(collateral, share, prevTotal, skim);\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\n }\n\n /// @dev Concrete implementation of `removeCollateral`.\n function _removeCollateral(address to, uint256 share) internal {\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(\n share\n );\n // No underflow: userTotalShare > userCollateralShare[msg.sender]\n // Cast safe: Bento transfer reverts if it is not.\n collateralBalance.userTotalShare -= uint128(share);\n emit LogRemoveCollateral(msg.sender, to, share);\n bentoBox.transfer(collateral, address(this), to, share);\n }\n\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\n /// @param to The receiver of the shares.\n /// @param share Amount of shares to remove.\n function removeCollateral(address to, uint256 share) public solvent {\n // accrue must be called because we check solvency\n accrue();\n _removeCollateral(to, share);\n }\n\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// @param toReservesShare Amount of shares to reserves.\n /// @param toReservesAmount Token amount. Ignored if `toReservesShare` nonzero.\n /// @param toFeesAmount Token fee amount. Ignored if `toReservesShare` nonzero.\n function _receiveAsset(\n bool skim,\n uint256 toReservesShare,\n // (There is no case where we pass along a fee in shares)\n uint256 toReservesAmount,\n uint256 toFeesAmount\n ) internal {\n IERC20 _asset = asset;\n AssetBalance memory _assetBalance = assetBalance;\n uint256 priorAssetTotalShare = _assetBalance.reservesShare +\n _assetBalance.feesEarnedShare;\n Rebase memory bentoBoxTotals = bentoBox.totals(_asset);\n\n uint256 toFeesShare = 0;\n if (toReservesShare == 0) {\n toReservesShare = bentoBoxTotals.toBase(toReservesAmount, true);\n if (toFeesAmount > 0) {\n toFeesShare = bentoBoxTotals.toBase(toFeesAmount, false);\n }\n }\n uint256 takenShare = toReservesShare.add(toFeesShare);\n\n if (_assetBalance.reservesShare == 0) {\n uint256 _feesOwedAmount = feesOwedAmount;\n if (_feesOwedAmount > 0) {\n uint256 feesOwedShare = bentoBoxTotals.toBase(\n _feesOwedAmount,\n false\n );\n // New fees cannot pay off existing fees:\n if (toReservesShare < feesOwedShare) {\n feesOwedAmount = bentoBoxTotals.toElastic(\n feesOwedShare - toReservesShare,\n false\n );\n _assetBalance.feesEarnedShare += uint128(takenShare);\n } else {\n feesOwedAmount = 0;\n // No overflow: assuming the transfer at the end succeeds:\n // feesOwedShare <= toReservesShare <= (Bento balance),\n _assetBalance.feesEarnedShare += uint128(\n feesOwedShare + toFeesShare\n );\n _assetBalance.reservesShare = uint128(\n toReservesShare - feesOwedShare\n );\n }\n } else {\n _assetBalance.reservesShare = uint128(toReservesShare);\n _assetBalance.feesEarnedShare += uint128(toFeesShare);\n }\n } else {\n _assetBalance.reservesShare += uint128(toReservesShare);\n _assetBalance.feesEarnedShare += uint128(toFeesShare);\n }\n assetBalance = _assetBalance;\n\n _addTokens(_asset, takenShare, priorAssetTotalShare, skim);\n }\n\n /// @dev Concrete implementation of `addAsset`.\n function _addAsset(bool skim, uint256 share) internal {\n _receiveAsset(skim, share, 0, 0);\n emit LogAddAsset(skim ? address(bentoBox) : msg.sender, share);\n }\n\n /// @notice Adds assets to the lending pair.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add.\n function addAsset(bool skim, uint256 share) public {\n accrue();\n _addAsset(skim, share);\n }\n\n /// @dev Concrete implementation of `removeAsset`.\n function _removeAsset(address to, uint256 share) internal {\n require(msg.sender == lender, \"PrivatePool: not the lender\");\n // Cast safe: Bento transfer reverts unless stronger condition holds\n assetBalance.reservesShare = assetBalance.reservesShare.sub(\n uint128(share)\n );\n bentoBox.transfer(asset, address(this), to, share);\n emit LogRemoveAsset(to, share);\n }\n\n /// @notice Removes an asset from msg.sender and transfers it to `to`.\n /// @param to The address that receives the removed assets.\n /// @param share The amount of shares to remove.\n function removeAsset(address to, uint256 share) public {\n accrue();\n _removeAsset(to, share);\n }\n\n /// @dev Concrete implementation of `borrow`.\n function _borrow(address to, uint256 amount)\n internal\n returns (uint256 part, uint256 share)\n {\n require(\n approvedBorrowers[msg.sender],\n \"PrivatePool: unapproved borrower\"\n );\n IERC20 _asset = asset;\n Rebase memory bentoBoxTotals = bentoBox.totals(_asset);\n AccrueInfo memory _accrueInfo = accrueInfo;\n\n share = bentoBoxTotals.toBase(amount, false);\n\n // No overflow: `share` is not modified any more, and fits in the\n // BentoBox shares total if the transfer succeeds. But then \"amount\"\n // must fit in the token total; at least up to some rounding error,\n // which cannot be more than 128 bits either.\n uint256 openFeeAmount = (amount * _accrueInfo.BORROW_OPENING_FEE_BPS) /\n BPS;\n // No overflow: Same reason. Also, we just divided by BPS..\n uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS;\n uint256 protocolFeeShare = bentoBoxTotals.toBase(\n protocolFeeAmount,\n false\n );\n\n // The protocol component of the opening fee cannot be owed:\n AssetBalance memory _assetBalance = assetBalance;\n // No overflow on the add: protocolFeeShare < share < Bento total, or\n // the transfer reverts. The transfer is independent of the results of\n // these calculations: `share` is not modified.\n // Theoretically the fee could just make it overflow 128 bits.\n // Underflow check is core business logic:\n _assetBalance.reservesShare = _assetBalance.reservesShare.sub(\n (share + protocolFeeShare).to128()\n );\n // Cast is safe: `share` fits. Also, the checked cast above succeeded.\n // No overflow: protocolFeeShare < reservesShare, and both balances\n // together fit in the Bento share balance,\n _assetBalance.feesEarnedShare += uint128(protocolFeeShare);\n assetBalance = _assetBalance;\n\n // No overflow (inner add): amount fits in 128 bits, as shown before,\n // and openFeeAmount is less.\n (totalDebt, part) = totalDebt.add(amount + openFeeAmount, true);\n // No overflow: totalDebt.base is the sum of all borrowerDebtParts,\n // fits in 128 bits and just had \"part\" added to it.\n borrowerDebtPart[msg.sender] += part;\n emit LogBorrow(msg.sender, to, amount, openFeeAmount, part);\n\n bentoBox.transfer(_asset, address(this), to, share);\n }\n\n /// @notice Sender borrows `amount` and transfers it to `to`.\n /// @return part Total part of the debt held by borrowers.\n /// @return share Total amount in shares borrowed.\n function borrow(address to, uint256 amount)\n public\n solvent\n returns (uint256 part, uint256 share)\n {\n accrue();\n (part, share) = _borrow(to, amount);\n }\n\n /// @dev Concrete implementation of `repay`.\n function _repay(\n address to,\n bool skim,\n uint256 part\n ) internal returns (uint256 amount) {\n (totalDebt, amount) = totalDebt.sub(part, true);\n borrowerDebtPart[to] = borrowerDebtPart[to].sub(part);\n _receiveAsset(skim, 0, amount, 0);\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\n }\n\n /// @notice Repays a loan.\n /// @param to Address of the borrower this payment should go.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param part The amount to repay as part. See `borrowerDebtPart`.\n /// @return amount The total amount repayed.\n function repay(\n address to,\n bool skim,\n uint256 part\n ) public returns (uint256 amount) {\n accrue();\n amount = _repay(to, skim, part);\n }\n\n // Functions that need accrue to be called\n uint8 internal constant ACTION_ADD_ASSET = 1;\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_ASSET = 3;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n uint8 internal constant ACTION_BORROW = 5;\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\n uint8 internal constant ACTION_ACCRUE = 8;\n\n // Functions that don't need accrue to be called\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0\n ? uint256(inNum)\n : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\n data,\n (IERC20, address, int256, int256)\n );\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return\n bentoBox.deposit{value: value}(\n token,\n msg.sender,\n to,\n uint256(amount),\n uint256(share)\n );\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\n data,\n (IERC20, address, int256, int256)\n );\n return\n bentoBox.withdraw(\n token,\n msg.sender,\n to,\n _num(amount, value1, value2),\n _num(share, value1, value2)\n );\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (\n address callee,\n bytes memory callData,\n bool useValue1,\n bool useValue2,\n uint8 returnValues\n ) = abi.decode(data, (address, bytes, bool, bool, uint8));\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(\n callee != address(bentoBox) && callee != address(this),\n \"PrivatePool: can't call\"\n );\n\n (bool success, bytes memory returnData) = callee.call{value: value}(\n callData\n );\n require(success, \"PrivatePool: call failed\");\n return (returnData, returnValues);\n }\n\n struct CookStatus {\n bool needsSolvencyCheck;\n bool hasAccrued;\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n CookStatus memory status;\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (!status.hasAccrued && action < 10) {\n accrue();\n status.hasAccrued = true;\n }\n if (action == ACTION_ADD_COLLATERAL) {\n (int256 share, address to, bool skim) = abi.decode(\n datas[i],\n (int256, address, bool)\n );\n addCollateral(to, skim, _num(share, value1, value2));\n } else if (action == ACTION_ADD_ASSET) {\n (int256 share, bool skim) = abi.decode(\n datas[i],\n (int256, bool)\n );\n _addAsset(skim, _num(share, value1, value2));\n } else if (action == ACTION_REPAY) {\n (int256 part, address to, bool skim) = abi.decode(\n datas[i],\n (int256, address, bool)\n );\n _repay(to, skim, _num(part, value1, value2));\n } else if (action == ACTION_REMOVE_ASSET) {\n (int256 share, address to) = abi.decode(\n datas[i],\n (int256, address)\n );\n _removeAsset(to, _num(share, value1, value2));\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (int256 share, address to) = abi.decode(\n datas[i],\n (int256, address)\n );\n _removeCollateral(to, _num(share, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_BORROW) {\n (int256 amount, address to) = abi.decode(\n datas[i],\n (int256, address)\n );\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\n (bool must_update, uint256 minRate, uint256 maxRate) = abi\n .decode(datas[i], (bool, uint256, uint256));\n (bool updated, uint256 rate) = updateExchangeRate();\n require(\n (!must_update || updated) &&\n rate > minRate &&\n (maxRate == 0 || rate > maxRate),\n \"PrivatePool: rate not ok\"\n );\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (\n address user,\n address _masterContract,\n bool approved,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(\n datas[i],\n (address, address, bool, uint8, bytes32, bytes32)\n );\n bentoBox.setMasterContractApproval(\n user,\n _masterContract,\n approved,\n v,\n r,\n s\n );\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(\n datas[i],\n values[i],\n value1,\n value2\n );\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(\n datas[i],\n (IERC20, address, int256)\n );\n bentoBox.transfer(\n token,\n msg.sender,\n to,\n _num(share, value1, value2)\n );\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (\n IERC20 token,\n address[] memory tos,\n uint256[] memory shares\n ) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(\n values[i],\n datas[i],\n value1,\n value2\n );\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(\n returnData,\n (uint256, uint256)\n );\n }\n } else if (action == ACTION_GET_REPAY_SHARE) {\n int256 part = abi.decode(datas[i], (int256));\n value1 = bentoBox.toShare(\n asset,\n totalDebt.toElastic(_num(part, value1, value2), true),\n true\n );\n } else if (action == ACTION_GET_REPAY_PART) {\n int256 amount = abi.decode(datas[i], (int256));\n value1 = totalDebt.toBase(_num(amount, value1, value2), false);\n }\n }\n\n if (status.needsSolvencyCheck) {\n require(_isSolvent(msg.sender), \"PrivatePool: borrower insolvent\");\n }\n }\n\n /// @notice Handles the liquidation of borrowers' balances, once the borrowers' amount of collateral is too low.\n /// @param borrowers An array of borrower addresses.\n /// @param maxDebtParts A one-to-one mapping to `borrowers`, contains maximum part (not token amount) of the debt that will be liquidated of the respective borrower.\n /// @param to Address of the receiver if `swapper` is zero.\n /// @param swapper Contract address of the `ISimpleSwapper` implementation.\n function liquidate(\n address[] calldata borrowers,\n uint256[] calldata maxDebtParts,\n address to,\n ISimpleSwapper swapper\n ) public {\n // Oracle can fail but we still need to allow liquidations\n (, uint256 _exchangeRate) = updateExchangeRate();\n accrue();\n\n AccrueInfo memory _accrueInfo = accrueInfo;\n\n uint256 allCollateralShare;\n uint256 allDebtAmount;\n uint256 allDebtPart;\n Rebase memory _totalDebt = totalDebt;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n\n for (uint256 i = 0; i < borrowers.length; i++) {\n // TODO: Find a way to not have it here; only for stack reasons\n address borrower = borrowers[i];\n // If we set an expiration at all, then by the above check it is\n // now past and every borrower can be liquidated at the current\n // price:\n if (\n block.timestamp >= accrueInfo.EXPIRATION ||\n !_isSolvent(borrower)\n ) {\n uint256 debtPart;\n {\n uint256 availableDebtPart = borrowerDebtPart[borrower];\n debtPart = maxDebtParts[i] > availableDebtPart\n ? availableDebtPart\n : maxDebtParts[i];\n // No underflow: ensured by definition of debtPart\n borrowerDebtPart[borrower] = availableDebtPart - debtPart;\n }\n uint256 debtAmount = _totalDebt.toElastic(debtPart, false);\n // No overflow (inner): debtAmount <= totalDebt.elastic < 2^128.\n // The exchange rate need not be reasonable: with an expiration\n // time set there is no _isSolvent() call.\n uint256 collateralShare = bentoBoxTotals.toBase(\n (debtAmount * _accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul(\n _exchangeRate\n ) / (BPS * EXCHANGE_RATE_PRECISION),\n false\n );\n\n // This needs to be updated here so that the same user cannot\n // be liquidated more than once. (Unless it adds up to one\n // \"full\" liquidation or less).\n // Underflow check is business logic: the liquidator can only\n // take enough to cover the loan (and bonus).\n userCollateralShare[borrower] = userCollateralShare[borrower]\n .sub(collateralShare);\n\n if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) {\n emit LogSeizeCollateral(\n borrower,\n collateralShare,\n debtAmount,\n debtPart\n );\n } else {\n emit LogRemoveCollateral(\n borrower,\n swapper == ISimpleSwapper(0) ? to : address(swapper),\n collateralShare\n );\n emit LogRepay(\n swapper == ISimpleSwapper(0)\n ? msg.sender\n : address(swapper),\n borrower,\n debtAmount,\n debtPart\n );\n }\n\n // No overflow in the below three:\n //\n // share(s) / amount(s) / part(s) involved in liquidation\n // <= total share / amount / part\n // <= (Bento).base / totalDebt.elastic / totalDebt.base\n // < 2^128\n //\n // Collateral share and debt part have already been\n // successfully subtracted from some user's balance (and this\n // persists across loop runs); the calculation for debt amount\n // rounds down, so it fits if debtPart fits. It follows that\n // the condition holds for the accumulated sums.\n allCollateralShare += collateralShare;\n allDebtAmount += debtAmount;\n allDebtPart += debtPart;\n }\n }\n require(allDebtAmount != 0, \"PrivatePool: all are solvent\");\n // No overflow (both): (liquidated debt) <= (total debt).\n // Cast is safe (both): (liquidated debt) <= (total debt) < 2^128\n _totalDebt.elastic -= uint128(allDebtAmount);\n _totalDebt.base -= uint128(allDebtPart);\n totalDebt = _totalDebt;\n\n if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) {\n // Unlike normal liquidations, the liquidator and the lender share\n // the excess. This compensates the lender for agreeing to payment\n // in the collateral. The protocol gets a cut of the total excess.\n // So the final distribution is\n // - X% excess (configurable via LIQUIDATION_MULTIPLIER_BPS)\n // - (10% of X) protocol fee\n // - (45% of X) liquidator share of bonus\n // - 100% + (45% of X) debt + lender share of bonus\n // allCollateralShare already includes the bonus, which in turn\n // includes the protocol fee. We round the bonus down to favor the\n // lender, and the fee to favor the liquidator and lender:\n // Math: All collateral fits in 128 bits (BentoBox), so the\n // multiplications are safe:\n uint256 excessShare = (allCollateralShare *\n (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) /\n _accrueInfo.LIQUIDATION_MULTIPLIER_BPS;\n uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS;\n uint256 liquidatorShare = (excessShare - feeShare) / 2;\n // We would add more variables for clarity, but stack depth:\n {\n CollateralBalance memory _collateralBalance = collateralBalance;\n // No underflow: All amounts fit in the collateral Bento total\n // The lender is also a \"user\", so only the fee and liquidator\n // share leave the user total \"account\":\n _collateralBalance.userTotalShare -= uint128(\n feeShare + liquidatorShare\n );\n _collateralBalance.feesEarnedShare += uint128(feeShare);\n collateralBalance = _collateralBalance;\n }\n // The rest goes to the lender:\n userCollateralShare[lender] += (allCollateralShare -\n feeShare -\n liquidatorShare);\n // The liquidator gets the other half -- rounded in their favour,\n // if applicable:\n bentoBox.transfer(collateral, address(this), to, liquidatorShare);\n } else {\n // No underflow: allCollateralShare is the sum of quantities that\n // have successfully been taken out of user balances.\n // Cast is safe: Above reason, and userTotalShare < 2^128\n collateralBalance.userTotalShare -= uint128(allCollateralShare);\n\n // Charge the protocol fee over the excess.\n // No overflow:\n // allDebtAmount <= totalDebt.elastic < 2^128 (proof in loop)\n // LIQUIDATION_MULTIPLIER_BPS < 2^16\n // PROTOCOL_FEE_BPS <= 10k < 2^14 (or we have bigger problems)\n uint256 feeAmount = (allDebtAmount *\n (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS) *\n PROTOCOL_FEE_BPS) / (BPS * BPS);\n\n // Swap using a swapper freely chosen by the caller\n // Open (flash) liquidation: get proceeds first and provide the\n // borrow after\n bentoBox.transfer(\n collateral,\n address(this),\n swapper == ISimpleSwapper(0) ? to : address(swapper),\n allCollateralShare\n );\n if (swapper != ISimpleSwapper(0)) {\n // TODO: Somehow split _receiveAsset to reduce loads?\n IERC20 _asset = asset;\n swapper.swap(\n collateral,\n _asset,\n msg.sender,\n bentoBox.toShare(\n _asset,\n allDebtAmount.add(feeAmount),\n true\n ),\n allCollateralShare\n );\n }\n _receiveAsset(false, 0, allDebtAmount, feeAmount);\n }\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n accrue();\n address to = masterContract.feeTo();\n\n uint256 assetShare = assetBalance.feesEarnedShare;\n if (assetShare > 0) {\n bentoBox.transfer(asset, address(this), to, assetShare);\n assetBalance.feesEarnedShare = 0;\n }\n\n uint256 collateralShare = collateralBalance.feesEarnedShare;\n if (collateralShare > 0) {\n bentoBox.transfer(collateral, address(this), to, collateralShare);\n collateralBalance.feesEarnedShare = 0;\n }\n\n emit LogWithdrawFees(to, assetShare, collateralShare);\n }\n\n /// @notice Sets the beneficiary of fees accrued in liquidations.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n}\n" + }, + "contracts/interfaces/ISimpleSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\n\ninterface ISimpleSwapper {\n /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for at least 'amountToMin' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Returns the amount of tokens 'to' transferred to BentoBox.\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) external returns (uint256 extraShare, uint256 shareReturned);\n}\n" + }, + "contracts/libraries/FullMath.sol": { + "content": "// SPDX-License-Identifier: CC-BY-4.0\npragma solidity 0.6.12;\n\n// solhint-disable\n\n// taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1\n// license is CC-BY-4.0\nlibrary FullMath {\n function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) {\n uint256 mm = mulmod(x, y, uint256(-1));\n l = x * y;\n h = mm - l;\n if (mm < l) h -= 1;\n }\n\n function fullDiv(\n uint256 l,\n uint256 h,\n uint256 d\n ) private pure returns (uint256) {\n uint256 pow2 = d & -d;\n d /= pow2;\n l /= pow2;\n l += h * ((-pow2) / pow2 + 1);\n uint256 r = 1;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n return l * r;\n }\n\n function mulDiv(\n uint256 x,\n uint256 y,\n uint256 d\n ) internal pure returns (uint256) {\n (uint256 l, uint256 h) = fullMul(x, y);\n uint256 mm = mulmod(x, y, d);\n if (mm > l) h -= 1;\n l -= mm;\n require(h < d, \"FullMath::mulDiv: overflow\");\n return fullDiv(l, h, d);\n }\n}\n" + }, + "contracts/libraries/FixedPoint.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0-or-later\npragma solidity 0.6.12;\nimport \"./FullMath.sol\";\n\n// solhint-disable\n\n// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))\nlibrary FixedPoint {\n // range: [0, 2**112 - 1]\n // resolution: 1 / 2**112\n struct uq112x112 {\n uint224 _x;\n }\n\n // range: [0, 2**144 - 1]\n // resolution: 1 / 2**112\n struct uq144x112 {\n uint256 _x;\n }\n\n uint8 private constant RESOLUTION = 112;\n uint256 private constant Q112 = 0x10000000000000000000000000000;\n uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000;\n uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits)\n\n // decode a UQ144x112 into a uint144 by truncating after the radix point\n function decode144(uq144x112 memory self) internal pure returns (uint144) {\n return uint144(self._x >> RESOLUTION);\n }\n\n // multiply a UQ112x112 by a uint256, returning a UQ144x112\n // reverts on overflow\n function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) {\n uint256 z = 0;\n require(y == 0 || (z = self._x * y) / y == self._x, \"FixedPoint::mul: overflow\");\n return uq144x112(z);\n }\n\n // returns a UQ112x112 which represents the ratio of the numerator to the denominator\n // lossy if either numerator or denominator is greater than 112 bits\n function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) {\n require(denominator > 0, \"FixedPoint::fraction: div by 0\");\n if (numerator == 0) return FixedPoint.uq112x112(0);\n\n if (numerator <= uint144(-1)) {\n uint256 result = (numerator << RESOLUTION) / denominator;\n require(result <= uint224(-1), \"FixedPoint::fraction: overflow\");\n return uq112x112(uint224(result));\n } else {\n uint256 result = FullMath.mulDiv(numerator, Q112, denominator);\n require(result <= uint224(-1), \"FixedPoint::fraction: overflow\");\n return uq112x112(uint224(result));\n }\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/BoringBatchable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\n// solhint-disable avoid-low-level-calls\n// solhint-disable no-inline-assembly\n\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\n// WARNING!!!\n// Combining BoringBatchable with msg.value can cause double spending issues\n// https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong/\n\nimport \"./interfaces/IERC20.sol\";\n\ncontract BaseBoringBatchable {\n /// @dev Helper function to extract a useful revert message from a failed call.\n /// If the returned data is malformed or not correctly abi encoded then this call can fail itself.\n function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {\n // If the _res length is less than 68, then the transaction failed silently (without a revert message)\n if (_returnData.length < 68) return \"Transaction reverted silently\";\n\n assembly {\n // Slice the sighash.\n _returnData := add(_returnData, 0x04)\n }\n return abi.decode(_returnData, (string)); // All that remains is the revert string\n }\n\n /// @notice Allows batched call to self (this contract).\n /// @param calls An array of inputs for each call.\n /// @param revertOnFail If True then reverts after a failed call and stops doing further calls.\n // F1: External is ok here because this is the batch function, adding it to a batch makes no sense\n // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value\n // C3: The length of the loop is fully under user control, so can't be exploited\n // C7: Delegatecall is only used on the same contract, so it's safe\n function batch(bytes[] calldata calls, bool revertOnFail) external payable {\n for (uint256 i = 0; i < calls.length; i++) {\n (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\n if (!success && revertOnFail) {\n revert(_getRevertMsg(result));\n }\n }\n }\n}\n\ncontract BoringBatchable is BaseBoringBatchable {\n /// @notice Call wrapper that performs `ERC20.permit` on `token`.\n /// Lookup `IERC20.permit`.\n // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert)\n // if part of a batch this could be used to grief once as the second call would not need the permit\n function permitToken(\n IERC20 token,\n address from,\n address to,\n uint256 amount,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n token.permit(from, to, amount, deadline, v, r, s);\n }\n}\n" + }, + "contracts/sSpell.sol": { + "content": "//SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/Domain.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringBatchable.sol\";\n\n\n// Staking in sSpell inspired by Chef Nomi's SushiBar - MIT license (originally WTFPL)\n// modified by BoringCrypto for DictatorDAO\n\ncontract sSpell is IERC20, Domain {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using BoringERC20 for IERC20;\n\n string public constant symbol = \"sSPELL\";\n string public constant name = \"Staked Spell Tokens\";\n uint8 public constant decimals = 18;\n uint256 public override totalSupply;\n uint256 private constant LOCK_TIME = 24 hours;\n\n IERC20 public immutable token;\n\n constructor(IERC20 _token) public {\n token = _token;\n }\n\n struct User {\n uint128 balance;\n uint128 lockedUntil;\n }\n\n /// @notice owner > balance mapping.\n mapping(address => User) public users;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public override allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n\n function balanceOf(address user) public view override returns (uint256 balance) {\n return users[user].balance;\n }\n\n function _transfer(\n address from,\n address to,\n uint256 shares\n ) internal {\n User memory fromUser = users[from];\n require(block.timestamp >= fromUser.lockedUntil, \"Locked\");\n if (shares != 0) {\n require(fromUser.balance >= shares, \"Low balance\");\n if (from != to) {\n require(to != address(0), \"Zero address\"); // Moved down so other failed calls safe some gas\n User memory toUser = users[to];\n users[from].balance = fromUser.balance - shares.to128(); // Underflow is checked\n users[to].balance = toUser.balance + shares.to128(); // Can't overflow because totalSupply would be greater than 2^128-1;\n }\n }\n emit Transfer(from, to, shares);\n }\n\n function _useAllowance(address from, uint256 shares) internal {\n if (msg.sender == from) {\n return;\n }\n uint256 spenderAllowance = allowance[from][msg.sender];\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\n if (spenderAllowance != type(uint256).max) {\n require(spenderAllowance >= shares, \"Low allowance\");\n allowance[from][msg.sender] = spenderAllowance - shares; // Underflow is checked\n }\n }\n\n /// @notice Transfers `shares` tokens from `msg.sender` to `to`.\n /// @param to The address to move the tokens.\n /// @param shares of the tokens to move.\n /// @return (bool) Returns True if succeeded.\n function transfer(address to, uint256 shares) public returns (bool) {\n _transfer(msg.sender, to, shares);\n return true;\n }\n\n /// @notice Transfers `shares` tokens from `from` to `to`. Caller needs approval for `from`.\n /// @param from Address to draw tokens from.\n /// @param to The address to move the tokens.\n /// @param shares The token shares to move.\n /// @return (bool) Returns True if succeeded.\n function transferFrom(\n address from,\n address to,\n uint256 shares\n ) public returns (bool) {\n _useAllowance(from, shares);\n _transfer(from, to, shares);\n return true;\n }\n\n /// @notice Approves `amount` from sender to be spend by `spender`.\n /// @param spender Address of the party that can draw from msg.sender's account.\n /// @param amount The maximum collective amount that `spender` can draw.\n /// @return (bool) Returns True if approved.\n function approve(address spender, uint256 amount) public override returns (bool) {\n allowance[msg.sender][spender] = amount;\n emit Approval(msg.sender, spender, amount);\n return true;\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\n\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\n /// @param owner_ Address of the owner.\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\n /// @param value The maximum collective amount that `spender` can draw.\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override {\n require(owner_ != address(0), \"Zero owner\");\n require(block.timestamp < deadline, \"Expired\");\n require(\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\n owner_,\n \"Invalid Sig\"\n );\n allowance[owner_][spender] = value;\n emit Approval(owner_, spender, value);\n }\n\n /// math is ok, because amount, totalSupply and shares is always 0 <= amount <= 100.000.000 * 10^18\n /// theoretically you can grow the amount/share ratio, but it's not practical and useless\n function mint(uint256 amount) public returns (bool) {\n require(msg.sender != address(0), \"Zero address\");\n User memory user = users[msg.sender];\n\n uint256 totalTokens = token.balanceOf(address(this));\n uint256 shares = totalSupply == 0 ? amount : (amount * totalSupply) / totalTokens;\n user.balance += shares.to128();\n user.lockedUntil = (block.timestamp + LOCK_TIME).to128();\n users[msg.sender] = user;\n totalSupply += shares;\n\n token.safeTransferFrom(msg.sender, address(this), amount);\n\n emit Transfer(address(0), msg.sender, shares);\n return true;\n }\n\n function _burn(\n address from,\n address to,\n uint256 shares\n ) internal {\n require(to != address(0), \"Zero address\");\n User memory user = users[from];\n require(block.timestamp >= user.lockedUntil, \"Locked\");\n uint256 amount = (shares * token.balanceOf(address(this))) / totalSupply;\n users[from].balance = user.balance.sub(shares.to128()); // Must check underflow\n totalSupply -= shares;\n\n token.safeTransfer(to, amount);\n\n emit Transfer(from, address(0), shares);\n }\n\n function burn(address to, uint256 shares) public returns (bool) {\n _burn(msg.sender, to, shares);\n return true;\n }\n\n function burnFrom(\n address from,\n address to,\n uint256 shares\n ) public returns (bool) {\n _useAllowance(from, shares);\n _burn(from, to, shares);\n return true;\n }\n}" + }, + "contracts/Spell.sol": { + "content": "// SPDX-License-Identifier: MIT\n\n// Spell\n\n// Special thanks to:\n// @BoringCrypto for his great libraries\n\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\n/// @title Spell\n/// @author 0xMerlin\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract Spell is ERC20, BoringOwnable {\n using BoringMath for uint256;\n // ERC20 'variables'\n string public constant symbol = \"SPELL\";\n string public constant name = \"Spell Token\";\n uint8 public constant decimals = 18;\n uint256 public override totalSupply;\n uint256 public constant MAX_SUPPLY = 420 * 1e27;\n\n function mint(address to, uint256 amount) public onlyOwner {\n require(to != address(0), \"SPELL: no mint to zero address\");\n require(MAX_SUPPLY >= totalSupply.add(amount), \"SPELL: Don't go over MAX\");\n\n totalSupply = totalSupply + amount;\n balanceOf[to] += amount;\n emit Transfer(address(0), to, amount);\n }\n}\n" + }, + "contracts/NFTPair.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Private Pool (NFT collateral)\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/Domain.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./interfaces/IERC721.sol\";\n\nstruct TokenLoanParams {\n uint128 valuation; // How much will you get? OK to owe until expiration.\n uint64 duration; // Length of loan in seconds\n uint16 annualInterestBPS; // Variable cost of taking out the loan\n}\n\ninterface ILendingClub {\n // Per token settings.\n function willLend(uint256 tokenId, TokenLoanParams memory params)\n external\n view\n returns (bool);\n\n function lendingConditions(address nftPair, uint256 tokenId)\n external\n view\n returns (TokenLoanParams memory);\n}\n\ninterface INFTPair {\n function collateral() external view returns (IERC721);\n\n function asset() external view returns (IERC20);\n\n function masterContract() external view returns (address);\n\n function bentoBox() external view returns (IBentoBoxV1);\n\n function removeCollateral(uint256 tokenId, address to) external;\n}\n\n/// @title NFTPair\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogRequestLoan(\n address indexed borrower,\n uint256 indexed tokenId,\n uint128 valuation,\n uint64 duration,\n uint16 annualInterestBPS\n );\n event LogUpdateLoanParams(\n uint256 indexed tokenId,\n uint128 valuation,\n uint64 duration,\n uint16 annualInterestBPS\n );\n // This automatically clears the associated loan, if any\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\n // Details are in the loan request\n event LogLend(address indexed lender, uint256 indexed tokenId);\n event LogRepay(address indexed from, uint256 indexed tokenId);\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n NFTPair public immutable masterContract;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC721 public collateral;\n IERC20 public asset;\n\n // A note on terminology:\n // \"Shares\" are BentoBox shares.\n\n // Track assets we own. Used to allow skimming the excesss.\n uint256 public feesEarnedShare;\n\n // Per token settings.\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\n\n uint8 private constant LOAN_INITIAL = 0;\n uint8 private constant LOAN_REQUESTED = 1;\n uint8 private constant LOAN_OUTSTANDING = 2;\n struct TokenLoan {\n address borrower;\n address lender;\n uint64 startTime;\n uint8 status;\n }\n mapping(uint256 => TokenLoan) public tokenLoan;\n\n // Do not go over 100% on either of these..\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\n uint256 private constant OPEN_FEE_BPS = 100;\n uint256 private constant BPS = 10_000;\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\n\n // Highest order term in the Maclaurin series for exp used by\n // `calculateIntest`.\n // Intuitive interpretation: interest continuously accrues on the principal.\n // That interest, in turn, earns \"second-order\" interest-on-interest, which\n // itself earns \"third-order\" interest, etc. This constant determines how\n // far we take this until we stop counting.\n //\n // The error, in terms of the interest rate, is at least\n //\n // ----- n ----- Infinity\n // \\ x^k \\ x^k\n // e^x - ) --- , which is ) --- ,\n // / k! / k!\n // ----- k = 1 k ----- k = n + 1\n //\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\n // interest that is owed at rate r over time t. It makes no difference if\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\n // is the same. Why \"at least\"? There are also rounding errors. See\n // `calculateInterest` for more detail.\n // The factorial in the denominator \"wins\"; for all reasonable (and quite\n // a few unreasonable) interest rates, the lower-order terms contribute the\n // most to the total. The following table lists some of the calculated\n // approximations for different values of n, along with the \"true\" result:\n //\n // Total: 10% 20% 50% 100% 200% 500% 1000%\n // -----------------------------------------------------------------------\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\n //\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\n //\n // For instance, calculating the compounding effects of 200% in \"total\"\n // interest to the sixth order results in 635.6%, whereas the true result\n // is 638.9%.\n // At 500% that difference is a little more dramatic, but it is still in\n // the same ballpark -- and of little practical consequence unless the\n // collateral can be expected to go up more than 112 times in value.\n // Still, for volatile tokens, or an asset that is somehow known to be very\n // inflationary, use a different number.\n // Zero (no interest at all) is ignored and treated as one (linear only).\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\n\n // For signed lend / borrow requests:\n mapping(address => uint256) public nonces;\n\n /// @notice The constructor is only used for the initial master contract.\n /// @notice Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n masterContract = this;\n }\n\n /// @notice De facto constructor for clone contracts\n function init(bytes calldata data) public payable override {\n require(\n address(collateral) == address(0),\n \"NFTPair: already initialized\"\n );\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\n require(address(collateral) != address(0), \"NFTPair: bad pair\");\n }\n\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params)\n public\n {\n TokenLoan memory loan = tokenLoan[tokenId];\n if (loan.status == LOAN_OUTSTANDING) {\n // The lender can change terms so long as the changes are strictly\n // the same or better for the borrower:\n require(msg.sender == loan.lender, \"NFTPair: not the lender\");\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\n require(\n params.duration >= cur.duration &&\n params.valuation <= cur.valuation &&\n params.annualInterestBPS <= cur.annualInterestBPS,\n \"NFTPair: worse params\"\n );\n } else if (loan.status == LOAN_REQUESTED) {\n // The borrower has already deposited the collateral and can\n // change whatever they like\n require(msg.sender == loan.borrower, \"NFTPair: not the borrower\");\n } else {\n // The loan has not been taken out yet; the borrower needs to\n // provide collateral.\n revert(\"NFTPair: no collateral\");\n }\n tokenLoanParams[tokenId] = params;\n emit LogUpdateLoanParams(\n tokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS\n );\n }\n\n function _requestLoan(\n address collateralProvider,\n uint256 tokenId,\n TokenLoanParams memory params,\n address to,\n bool skim\n ) private {\n // Edge case: valuation can be zero. That effectively gifts the NFT and\n // is therefore a bad idea, but does not break the contract.\n require(\n tokenLoan[tokenId].status == LOAN_INITIAL,\n \"NFTPair: loan exists\"\n );\n if (skim) {\n require(\n collateral.ownerOf(tokenId) == address(this),\n \"NFTPair: skim failed\"\n );\n } else {\n collateral.transferFrom(collateralProvider, address(this), tokenId);\n }\n TokenLoan memory loan;\n loan.borrower = to;\n loan.status = LOAN_REQUESTED;\n tokenLoan[tokenId] = loan;\n tokenLoanParams[tokenId] = params;\n\n emit LogRequestLoan(\n to,\n tokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS\n );\n }\n\n /// @notice Deposit an NFT as collateral and request a loan against it\n /// @param tokenId ID of the NFT\n /// @param to Address to receive the loan, or option to withdraw collateral\n /// @param params Loan conditions on offer\n /// @param skim True if the token has already been transfered\n function requestLoan(\n uint256 tokenId,\n TokenLoanParams memory params,\n address to,\n bool skim\n ) public {\n _requestLoan(msg.sender, tokenId, params, to, skim);\n }\n\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\n /// @notice This destroys the loan.\n /// @param tokenId The token\n /// @param to The receiver of the token.\n function removeCollateral(uint256 tokenId, address to) public {\n TokenLoan memory loan = tokenLoan[tokenId];\n if (loan.status == LOAN_REQUESTED) {\n // We are withdrawing collateral that is not in use:\n require(msg.sender == loan.borrower, \"NFTPair: not the borrower\");\n } else if (loan.status == LOAN_OUTSTANDING) {\n // We are seizing collateral as the lender. The loan has to be\n // expired and not paid off:\n require(msg.sender == loan.lender, \"NFTPair: not the lender\");\n require(\n // Addition is safe: both summands are smaller than 256 bits\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <=\n block.timestamp,\n \"NFTPair: not expired\"\n );\n }\n // If there somehow is collateral but no accompanying loan, then anyone\n // can claim it by first requesting a loan with `skim` set to true, and\n // then withdrawing. So we might as well allow it here..\n delete tokenLoan[tokenId];\n collateral.transferFrom(address(this), to, tokenId);\n emit LogRemoveCollateral(tokenId, to);\n }\n\n // Assumes the lender has agreed to the loan.\n function _lend(\n address lender,\n uint256 tokenId,\n TokenLoanParams memory accepted,\n bool skim\n ) internal {\n TokenLoan memory loan = tokenLoan[tokenId];\n require(loan.status == LOAN_REQUESTED, \"NFTPair: not available\");\n TokenLoanParams memory params = tokenLoanParams[tokenId];\n\n // Valuation has to be an exact match, everything else must be at least\n // as good for the lender as `accepted`.\n require(\n params.valuation == accepted.valuation &&\n params.duration <= accepted.duration &&\n params.annualInterestBPS >= accepted.annualInterestBPS,\n \"NFTPair: bad params\"\n );\n\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\n\n if (skim) {\n require(\n bentoBox.balanceOf(asset, address(this)) >=\n (totalShare -\n openFeeShare +\n protocolFeeShare +\n feesEarnedShare),\n \"NFTPair: skim too much\"\n );\n } else {\n bentoBox.transfer(\n asset,\n lender,\n address(this),\n totalShare - openFeeShare + protocolFeeShare\n );\n }\n // No underflow: follows from OPEN_FEE_BPS <= BPS\n uint256 borrowerShare = totalShare - openFeeShare;\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\n // No overflow: addends (and result) must fit in BentoBox\n feesEarnedShare += protocolFeeShare;\n\n loan.lender = lender;\n loan.status = LOAN_OUTSTANDING;\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\n tokenLoan[tokenId] = loan;\n\n emit LogLend(lender, tokenId);\n }\n\n /// @notice Lends with the parameters specified by the borrower.\n /// @param tokenId ID of the token that will function as collateral\n /// @param accepted Loan parameters as the lender saw them, for security\n /// @param skim True if the funds have been transfered to the contract\n function lend(\n uint256 tokenId,\n TokenLoanParams memory accepted,\n bool skim\n ) public {\n _lend(msg.sender, tokenId, accepted, skim);\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // NOTE on signature hashes: the domain separator only guarantees that the\n // chain ID and master contract are a match, so we explicitly include the\n // clone address (and the asset/collateral addresses):\n\n // keccak256(\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\")\n bytes32 private constant LEND_SIGNATURE_HASH =\n 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\n\n // keccak256(\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\")\n bytes32 private constant BORROW_SIGNATURE_HASH =\n 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\n\n /// @notice Request and immediately borrow from a pre-committed lender\n\n /// @notice Caller provides collateral; loan can go to a different address.\n /// @param tokenId ID of the token that will function as collateral\n /// @param lender Lender, whose BentoBox balance the funds will come from\n /// @param recipient Address to receive the loan.\n /// @param params Loan parameters requested, and signed by the lender\n /// @param skimCollateral True if the collateral has already been transfered\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\n function requestAndBorrow(\n uint256 tokenId,\n address lender,\n address recipient,\n TokenLoanParams memory params,\n bool skimCollateral,\n bool anyTokenId,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\n require(\n ILendingClub(lender).willLend(tokenId, params),\n \"NFTPair: LendingClub does not like you\"\n );\n } else {\n require(block.timestamp <= deadline, \"NFTPair: signature expired\");\n uint256 nonce = nonces[lender]++;\n bytes32 dataHash = keccak256(\n abi.encode(\n LEND_SIGNATURE_HASH,\n address(this),\n anyTokenId ? 0 : tokenId,\n anyTokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS,\n nonce,\n deadline\n )\n );\n require(\n ecrecover(_getDigest(dataHash), v, r, s) == lender,\n \"NFTPair: signature invalid\"\n );\n }\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\n _lend(lender, tokenId, params, false);\n }\n\n /// @notice Take collateral from a pre-commited borrower and lend against it\n /// @notice Collateral must come from the borrower, not a third party.\n /// @param tokenId ID of the token that will function as collateral\n /// @param borrower Address that provides collateral and receives the loan\n /// @param params Loan terms offered, and signed by the borrower\n /// @param skimFunds True if the funds have been transfered to the contract\n function takeCollateralAndLend(\n uint256 tokenId,\n address borrower,\n TokenLoanParams memory params,\n bool skimFunds,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n require(block.timestamp <= deadline, \"NFTPair: signature expired\");\n uint256 nonce = nonces[borrower]++;\n bytes32 dataHash = keccak256(\n abi.encode(\n BORROW_SIGNATURE_HASH,\n address(this),\n tokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS,\n nonce,\n deadline\n )\n );\n require(\n ecrecover(_getDigest(dataHash), v, r, s) == borrower,\n \"NFTPair: signature invalid\"\n );\n _requestLoan(borrower, tokenId, params, borrower, false);\n _lend(msg.sender, tokenId, params, skimFunds);\n }\n\n /// Approximates continuous compounding. Uses Horner's method to evaluate\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\n /// errors along the way. The following is always guaranteed:\n ///\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\n ///\n /// where time = t/YEAR, up to at most the rounding error obtained in\n /// calculating linear interest.\n ///\n /// If the theoretical result that we are approximating (the rightmost part\n /// of the above inquality) fits in 128 bits, then the function is\n /// guaranteed not to revert (unless n > 250, which is way too high).\n /// If even the linear interest (leftmost part of the inequality) does not\n /// the function will revert.\n /// Otherwise, the function may revert, return a reasonable result, or\n /// return a very inaccurate result. Even then the above inequality is\n /// respected.\n function calculateInterest(\n uint256 principal,\n uint64 t,\n uint16 aprBPS\n ) public pure returns (uint256 interest) {\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\n //\n // We calculate\n //\n // ----- n ----- n\n // \\ principal * (t * aprBPS)^k \\\n // ) -------------------------- =: ) term_k\n // / k! * YEAR_BPS^k /\n // ----- k = 1 ----- k = 1\n //\n // which approaches, but never exceeds the \"theoretical\" result,\n //\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\n //\n // as n goes to infinity. We use the fact that\n //\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\n // term_k = ---------------------------------------------\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\n //\n // t * aprBPS\n // = term_{k-1} * ------------ (*)\n // k * YEAR_BPS\n //\n // to calculate the terms one by one. The principal affords us the\n // precision to carry out the division without resorting to fixed-point\n // math. Any rounding error is downward, which we consider acceptable.\n //\n // Since all numbers involved are positive, each term is certainly\n // bounded above by M. From (*) we see that any intermediate results\n // are at most\n //\n // denom_k := k * YEAR_BPS.\n //\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\n // which proves that all calculations will certainly not overflow if M\n // fits in 128 bits.\n //\n // If M does not fit, then the intermediate results for some term may\n // eventually overflow, but this cannot happen at the first term, and\n // neither can the total overflow because it uses checked math.\n //\n // This constitutes a guarantee of specified behavior when M >= 2^128.\n uint256 x = uint256(t) * aprBPS;\n uint256 term_k = (principal * x) / YEAR_BPS;\n uint256 denom_k = YEAR_BPS;\n\n interest = term_k;\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\n denom_k += YEAR_BPS;\n term_k = (term_k * x) / denom_k;\n interest = interest.add(term_k); // <- Only overflow check we need\n }\n\n if (interest >= 2**128) {\n revert();\n }\n }\n\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\n TokenLoan memory loan = tokenLoan[tokenId];\n require(loan.status == LOAN_OUTSTANDING, \"NFTPair: no loan\");\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\n require(\n // Addition is safe: both summands are smaller than 256 bits\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\n \"NFTPair: loan expired\"\n );\n\n uint128 principal = loanParams.valuation;\n\n // No underflow: loan.startTime is only ever set to a block timestamp\n // Cast is safe: if this overflows, then all loans have expired anyway\n uint256 interest = calculateInterest(\n principal,\n uint64(block.timestamp - loan.startTime),\n loanParams.annualInterestBPS\n ).to128();\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\n amount = principal + interest;\n\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\n\n address from;\n if (skim) {\n require(\n bentoBox.balanceOf(asset, address(this)) >=\n (totalShare + feesEarnedShare),\n \"NFTPair: skim too much\"\n );\n from = address(this);\n // No overflow: result fits in BentoBox\n } else {\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\n from = msg.sender;\n }\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\n feesEarnedShare += feeShare;\n delete tokenLoan[tokenId];\n\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\n collateral.transferFrom(address(this), loan.borrower, tokenId);\n\n emit LogRepay(from, tokenId);\n }\n\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\n uint8 internal constant ACTION_LEND = 13;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n // Signed requests\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0\n ? uint256(inNum)\n : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\n data,\n (IERC20, address, int256, int256)\n );\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return\n bentoBox.deposit{value: value}(\n token,\n msg.sender,\n to,\n uint256(amount),\n uint256(share)\n );\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\n data,\n (IERC20, address, int256, int256)\n );\n return\n bentoBox.withdraw(\n token,\n msg.sender,\n to,\n _num(amount, value1, value2),\n _num(share, value1, value2)\n );\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (\n address callee,\n bytes memory callData,\n bool useValue1,\n bool useValue2,\n uint8 returnValues\n ) = abi.decode(data, (address, bytes, bool, bool, uint8));\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(\n callee != address(bentoBox) &&\n callee != address(collateral) &&\n callee != address(this),\n \"NFTPair: can't call\"\n );\n\n (bool success, bytes memory returnData) = callee.call{value: value}(\n callData\n );\n require(success, \"NFTPair: call failed\");\n return (returnData, returnValues);\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (action == ACTION_REPAY) {\n (uint256 tokenId, bool skim) = abi.decode(\n datas[i],\n (uint256, bool)\n );\n repay(tokenId, skim);\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (uint256 tokenId, address to) = abi.decode(\n datas[i],\n (uint256, address)\n );\n removeCollateral(tokenId, to);\n } else if (action == ACTION_REQUEST_LOAN) {\n (\n uint256 tokenId,\n TokenLoanParams memory params,\n address to,\n bool skim\n ) = abi.decode(\n datas[i],\n (uint256, TokenLoanParams, address, bool)\n );\n requestLoan(tokenId, params, to, skim);\n } else if (action == ACTION_LEND) {\n (\n uint256 tokenId,\n TokenLoanParams memory params,\n bool skim\n ) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\n lend(tokenId, params, skim);\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (\n address user,\n address _masterContract,\n bool approved,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(\n datas[i],\n (address, address, bool, uint8, bytes32, bytes32)\n );\n bentoBox.setMasterContractApproval(\n user,\n _masterContract,\n approved,\n v,\n r,\n s\n );\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(\n datas[i],\n values[i],\n value1,\n value2\n );\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(\n datas[i],\n (IERC20, address, int256)\n );\n bentoBox.transfer(\n token,\n msg.sender,\n to,\n _num(share, value1, value2)\n );\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (\n IERC20 token,\n address[] memory tos,\n uint256[] memory shares\n ) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(\n values[i],\n datas[i],\n value1,\n value2\n );\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(\n returnData,\n (uint256, uint256)\n );\n }\n } else if (action == ACTION_REQUEST_AND_BORROW) {\n (\n uint256 tokenId,\n address lender,\n address recipient,\n TokenLoanParams memory params,\n bool skimCollateral,\n bool anyTokenId,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(\n datas[i],\n (\n uint256,\n address,\n address,\n TokenLoanParams,\n bool,\n bool,\n uint256,\n uint8,\n bytes32,\n bytes32\n )\n );\n requestAndBorrow(\n tokenId,\n lender,\n recipient,\n params,\n skimCollateral,\n anyTokenId,\n deadline,\n v,\n r,\n s\n );\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\n (\n uint256 tokenId,\n address borrower,\n TokenLoanParams memory params,\n bool skimFunds,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(\n datas[i],\n (\n uint256,\n address,\n TokenLoanParams,\n bool,\n uint256,\n uint8,\n bytes32,\n bytes32\n )\n );\n takeCollateralAndLend(\n tokenId,\n borrower,\n params,\n skimFunds,\n deadline,\n v,\n r,\n s\n );\n }\n }\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n address to = masterContract.feeTo();\n\n uint256 _share = feesEarnedShare;\n if (_share > 0) {\n bentoBox.transfer(asset, address(this), to, _share);\n feesEarnedShare = 0;\n }\n\n emit LogWithdrawFees(to, _share);\n }\n\n /// @notice Sets the beneficiary of fees accrued in liquidations.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n}\n" + }, + "contracts/interfaces/IERC721.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Taken from OpenZeppelin contracts v3\n\npragma solidity >=0.6.2 <0.8.0;\n\nimport \"./IERC165.sol\";\n\n/**\n * @dev Required interface of an ERC721 compliant contract.\n */\ninterface IERC721 is IERC165 {\n /**\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\n */\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\n */\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\n */\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n\n /**\n * @dev Returns the number of tokens in ``owner``'s account.\n */\n function balanceOf(address owner) external view returns (uint256 balance);\n\n /**\n * @dev Returns the owner of the `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function ownerOf(uint256 tokenId) external view returns (address owner);\n\n /**\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must exist and be owned by `from`.\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\n *\n * Emits a {Transfer} event.\n */\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\n\n /**\n * @dev Transfers `tokenId` token from `from` to `to`.\n *\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must be owned by `from`.\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 tokenId) external;\n\n /**\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\n * The approval is cleared when the token is transferred.\n *\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\n *\n * Requirements:\n *\n * - The caller must own the token or be an approved operator.\n * - `tokenId` must exist.\n *\n * Emits an {Approval} event.\n */\n function approve(address to, uint256 tokenId) external;\n\n /**\n * @dev Returns the account approved for `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function getApproved(uint256 tokenId) external view returns (address operator);\n\n /**\n * @dev Approve or remove `operator` as an operator for the caller.\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\n *\n * Requirements:\n *\n * - The `operator` cannot be the caller.\n *\n * Emits an {ApprovalForAll} event.\n */\n function setApprovalForAll(address operator, bool _approved) external;\n\n /**\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\n *\n * See {setApprovalForAll}\n */\n function isApprovedForAll(address owner, address operator) external view returns (bool);\n\n /**\n * @dev Safely transfers `tokenId` token from `from` to `to`.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must exist and be owned by `from`.\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\n *\n * Emits a {Transfer} event.\n */\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\n}\n" + }, + "contracts/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Taken from OpenZeppelin contracts v3\n\npragma solidity >=0.6.0 <0.8.0;\n\n/**\n * @dev Interface of the ERC165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "contracts/helpers/YearnLiquidityMigrationHelper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface IYearnVault {\n function deposit(uint256 amount, address recipient) external returns (uint256 shares);\n}\n\ncontract YearnLiquidityMigrationHelper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n }\n\n function migrate(\n IERC20 token,\n IYearnVault vault,\n uint256 amount,\n address recipient\n ) external {\n token.approve(address(vault), amount);\n uint256 shares = vault.deposit(amount, address(bentoBox));\n bentoBox.deposit(token, address(bentoBox), recipient, shares, 0);\n }\n}\n" + }, + "contracts/mocks/SimpleStrategyMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\n\n// solhint-disable not-rely-on-time\n\ncontract SimpleStrategyMock is IStrategy {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n IERC20 private immutable token;\n address private immutable bentoBox;\n\n modifier onlyBentoBox() {\n require(msg.sender == bentoBox, \"Ownable: caller is not the owner\");\n _;\n }\n\n constructor(address bentoBox_, IERC20 token_) public {\n bentoBox = bentoBox_;\n token = token_;\n }\n\n // Send the assets to the Strategy and call skim to invest them\n function skim(uint256) external override onlyBentoBox {\n // Leave the tokens on the contract\n return;\n }\n\n // Harvest any profits made converted to the asset and pass them to the caller\n function harvest(uint256 balance, address) external override onlyBentoBox returns (int256 amountAdded) {\n amountAdded = int256(token.balanceOf(address(this)).sub(balance));\n token.safeTransfer(bentoBox, uint256(amountAdded)); // Add as profit\n }\n\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding or if the request was more than there is.\n function withdraw(uint256 amount) external override onlyBentoBox returns (uint256 actualAmount) {\n token.safeTransfer(bentoBox, uint256(amount)); // Add as profit\n actualAmount = amount;\n }\n\n // Withdraw all assets in the safest way possible. This shouldn't fail.\n function exit(uint256 balance) external override onlyBentoBox returns (int256 amountAdded) {\n amountAdded = 0;\n token.safeTransfer(bentoBox, balance);\n }\n}\n" + }, + "contracts/mocks/OracleMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// WARNING: This oracle is only for testing, please use PeggedOracle for a fixed value oracle\ncontract OracleMock is IOracle {\n using BoringMath for uint256;\n\n uint256 public rate;\n bool public success;\n\n constructor() public {\n success = true;\n }\n\n function set(uint256 rate_) public {\n // The rate can be updated.\n rate = rate_;\n }\n\n function setSuccess(bool val) public {\n success = val;\n }\n\n function getDataParameter() public pure returns (bytes memory) {\n return abi.encode(\"0x0\");\n }\n\n // Get the latest exchange rate\n function get(bytes calldata) public override returns (bool, uint256) {\n return (success, rate);\n }\n\n // Check the last exchange rate without any state changes\n function peek(bytes calldata) public view override returns (bool, uint256) {\n return (success, rate);\n }\n\n function peekSpot(bytes calldata) public view override returns (uint256) {\n return rate;\n }\n\n function name(bytes calldata) public view override returns (string memory) {\n return \"Test\";\n }\n\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"TEST\";\n }\n}\n" + }, + "contracts/mocks/ExternalFunctionMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\ncontract ExternalFunctionMock {\n using BoringMath for uint256;\n\n event Result(uint256 output);\n\n function sum(uint256 a, uint256 b) external returns (uint256 c) {\n c = a.add(b);\n emit Result(c);\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"./libraries/BoringAddress.sol\";\nimport \"./libraries/BoringMath.sol\";\nimport \"./interfaces/IERC721TokenReceiver.sol\";\n\n// solhint-disable avoid-low-level-calls\n\nabstract contract BoringMultipleNFT {\n /// This contract is an EIP-721 compliant contract with enumerable support\n /// To optimize for gas, tokenId is sequential and start at 0. Also, tokens can't be removed/burned.\n using BoringAddress for address;\n using BoringMath for uint256;\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n\n uint256 public totalSupply = 0;\n\n struct TokenInfo {\n address owner;\n uint24 index; // index in the tokensOf array, one address can hold a maximum of 16,777,216 tokens\n uint72 data; // data field can be usse to store traits\n }\n\n // operator mappings as per usual\n mapping(address => mapping(address => bool)) public isApprovedForAll;\n mapping(address => uint256[]) public tokensOf; // Array of tokens owned by\n mapping(uint256 => TokenInfo) internal _tokens; // The index in the tokensOf array for the token, needed to remove tokens from tokensOf\n mapping(uint256 => address) internal _approved; // keep track of approved nft\n\n function supportsInterface(bytes4 interfaceID) external pure returns (bool) {\n return\n interfaceID == this.supportsInterface.selector || // EIP-165\n interfaceID == 0x80ac58cd; // EIP-721\n }\n\n function approve(address approved, uint256 tokenId) public payable {\n address owner = _tokens[tokenId].owner;\n require(msg.sender == owner || isApprovedForAll[owner][msg.sender], \"Not allowed\");\n _approved[tokenId] = approved;\n emit Approval(owner, approved, tokenId);\n }\n\n function getApproved(uint256 tokenId) public view returns (address approved) {\n require(tokenId < totalSupply, \"Invalid tokenId\");\n return _approved[tokenId];\n }\n\n function setApprovalForAll(address operator, bool approved) public {\n isApprovedForAll[msg.sender][operator] = approved;\n emit ApprovalForAll(msg.sender, operator, approved);\n }\n\n function ownerOf(uint256 tokenId) public view returns (address) {\n address owner = _tokens[tokenId].owner;\n require(owner != address(0), \"No owner\");\n return owner;\n }\n\n function balanceOf(address owner) public view returns (uint256) {\n require(owner != address(0), \"No 0 owner\");\n return tokensOf[owner].length;\n }\n\n function _transferBase(\n uint256 tokenId,\n address from,\n address to,\n uint72 data\n ) internal {\n address owner = _tokens[tokenId].owner;\n require(from == owner, \"From not owner\");\n\n uint24 index;\n // Remove the token from the current owner's tokensOf array\n if (from != address(0)) {\n index = _tokens[tokenId].index; // The index of the item to remove in the array\n data = _tokens[tokenId].data;\n uint256 last = tokensOf[from].length - 1;\n uint256 lastTokenId = tokensOf[from][last];\n tokensOf[from][index] = lastTokenId; // Copy the last item into the slot of the one to be removed\n _tokens[lastTokenId].index = index; // Update the token index for the last item that was moved\n tokensOf[from].pop(); // Delete the last item\n }\n\n index = uint24(tokensOf[to].length);\n tokensOf[to].push(tokenId);\n _tokens[tokenId] = TokenInfo({owner: to, index: index, data: data});\n\n // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event.\n _approved[tokenId] = address(0);\n emit Transfer(from, to, tokenId);\n }\n\n function _transfer(\n address from,\n address to,\n uint256 tokenId\n ) internal {\n require(msg.sender == from || msg.sender == _approved[tokenId] || isApprovedForAll[from][msg.sender], \"Transfer not allowed\");\n require(to != address(0), \"No zero address\");\n // check for owner == from is in base\n _transferBase(tokenId, from, to, 0);\n }\n\n function transferFrom(\n address from,\n address to,\n uint256 tokenId\n ) public payable {\n _transfer(from, to, tokenId);\n }\n\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId\n ) public payable {\n safeTransferFrom(from, to, tokenId, \"\");\n }\n\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId,\n bytes memory data\n ) public payable {\n _transfer(from, to, tokenId);\n if (to.isContract()) {\n require(\n IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) ==\n bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\")),\n \"Wrong return value\"\n );\n }\n }\n\n function tokenURI(uint256 tokenId) public view returns (string memory) {\n require(tokenId < totalSupply, \"Not minted\");\n return _tokenURI(tokenId);\n }\n\n function _tokenURI(uint256 tokenId) internal view virtual returns (string memory);\n\n function tokenByIndex(uint256 index) public view returns (uint256) {\n require(index < totalSupply, \"Out of bounds\");\n return index; // This works due the optimization of sequential tokenIds and no burning\n }\n\n function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) {\n return tokensOf[owner][index];\n }\n\n function _mint(address owner, uint72 data) internal {\n _transferBase(totalSupply, address(0), owner, data);\n totalSupply++;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol": { + "content": "// SPDX-License-Identifier: MIT\n\n//SPDX-License-Identifier: MIT\npragma solidity ^0.6.12;\n\n// solhint-disable no-inline-assembly\n\nlibrary BoringAddress {\n function isContract(address account) internal view returns (bool) {\n uint256 size;\n assembly {\n size := extcodesize(account)\n }\n return size > 0;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IERC721TokenReceiver.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IERC721TokenReceiver {\n function onERC721Received(\n address _operator,\n address _from,\n uint256 _tokenId,\n bytes calldata _data\n ) external returns (bytes4);\n}\n" + }, + "contracts/mocks/ERC721Mock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\";\n\ncontract ERC721Mock is BoringMultipleNFT {\n function mint(address owner) public returns (uint256 id) {\n id = totalSupply;\n _mint(owner, 0);\n }\n\n function _tokenURI(uint256) internal view override returns (string memory) {\n return \"\";\n }\n}\n" + }, + "contracts/CauldronV2MultiChain.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract CauldronV2MultiChain is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n CauldronV2MultiChain public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n uint64 INTEREST_PER_SECOND;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 public COLLATERIZATION_RATE;\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 public LIQUIDATION_MULTIPLIER; \r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 public BORROW_OPENING_FEE;\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n uint256 private constant DISTRIBUTION_PART = 10;\r\n uint256 private constant DISTRIBUTION_PRECISION = 100;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n // Apply a percentual fee share to sSpell holders\r\n \r\n {\r\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\r\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\r\n }\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), masterContract.owner(), amount, 0);\r\n }\r\n}\r\n" + }, + "contracts/CauldronV2Checkpoint.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Cauldron\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./MagicInternetMoney.sol\";\nimport \"./interfaces/IOracle.sol\";\nimport \"./interfaces/ISwapper.sol\";\nimport \"./interfaces/ICheckpointToken.sol\";\n\n// solhint-disable avoid-low-level-calls\n// solhint-disable no-inline-assembly\n\n/// @title Cauldron\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract CauldronV2Checkpoint is BoringOwnable, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogExchangeRate(uint256 rate);\n event LogAccrue(uint128 accruedAmount);\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n CauldronV2Checkpoint public immutable masterContract;\n IERC20 public immutable magicInternetMoney;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC20 public collateral;\n IOracle public oracle;\n bytes public oracleData;\n\n // Total amounts\n uint256 public totalCollateralShare; // Total collateral supplied\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\n\n // User balances\n mapping(address => uint256) public userCollateralShare;\n mapping(address => uint256) public userBorrowPart;\n\n /// @notice Exchange and interest rate tracking.\n /// This is 'cached' here because calls to Oracles can be very expensive.\n uint256 public exchangeRate;\n\n struct AccrueInfo {\n uint64 lastAccrued;\n uint128 feesEarned;\n uint64 INTEREST_PER_SECOND;\n }\n\n AccrueInfo public accrueInfo;\n\n // Settings\n uint256 public COLLATERIZATION_RATE;\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\n\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\n\n uint256 public LIQUIDATION_MULTIPLIER; \n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\n\n uint256 public BORROW_OPENING_FEE;\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\n\n uint256 private constant DISTRIBUTION_PART = 10;\n uint256 private constant DISTRIBUTION_PRECISION = 100;\n\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\n bentoBox = bentoBox_;\n magicInternetMoney = magicInternetMoney_;\n masterContract = this;\n }\n\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\n function init(bytes calldata data) public payable override {\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\n }\n\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\n function accrue() public {\n AccrueInfo memory _accrueInfo = accrueInfo;\n // Number of seconds since accrue was called\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\n if (elapsedTime == 0) {\n return;\n }\n _accrueInfo.lastAccrued = uint64(block.timestamp);\n\n Rebase memory _totalBorrow = totalBorrow;\n if (_totalBorrow.base == 0) {\n accrueInfo = _accrueInfo;\n return;\n }\n\n // Accrue interest\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\n\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\n totalBorrow = _totalBorrow;\n accrueInfo = _accrueInfo;\n\n emit LogAccrue(extraAmount);\n }\n\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\n // accrue must have already been called!\n uint256 borrowPart = userBorrowPart[user];\n if (borrowPart == 0) return true;\n uint256 collateralShare = userCollateralShare[user];\n if (collateralShare == 0) return false;\n\n Rebase memory _totalBorrow = totalBorrow;\n\n return\n bentoBox.toAmount(\n collateral,\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\n false\n ) >=\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\n }\n\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\n modifier solvent() {\n _;\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\n }\n\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\n /// @return updated True if `exchangeRate` was updated.\n /// @return rate The new exchange rate.\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\n (updated, rate) = oracle.get(oracleData);\n\n if (updated) {\n exchangeRate = rate;\n emit LogExchangeRate(rate);\n } else {\n // Return the old rate if fetching wasn't successful\n rate = exchangeRate;\n }\n }\n\n /// @dev Helper function to move tokens.\n /// @param token The ERC-20 token.\n /// @param share The amount in shares to add.\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\n /// Only used for accounting checks.\n /// @param skim If True, only does a balance check on this contract.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n function _addTokens(\n IERC20 token,\n uint256 share,\n uint256 total,\n bool skim\n ) internal {\n if (skim) {\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\n } else {\n bentoBox.transfer(token, msg.sender, address(this), share);\n }\n }\n\n /// @notice Adds `collateral` from msg.sender to the account `to`.\n /// @param to The receiver of the tokens.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add for `to`.\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) public {\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([to,address(0)]);\n\n userCollateralShare[to] = userCollateralShare[to].add(share);\n uint256 oldTotalCollateralShare = totalCollateralShare;\n totalCollateralShare = oldTotalCollateralShare.add(share);\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\n }\n\n /// @dev Concrete implementation of `removeCollateral`.\n function _removeCollateral(address to, uint256 share) internal {\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([address(msg.sender),address(0)]);\n\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\n totalCollateralShare = totalCollateralShare.sub(share);\n emit LogRemoveCollateral(msg.sender, to, share);\n bentoBox.transfer(collateral, address(this), to, share);\n }\n\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\n /// @param to The receiver of the shares.\n /// @param share Amount of shares to remove.\n function removeCollateral(address to, uint256 share) public solvent {\n // accrue must be called because we check solvency\n accrue();\n _removeCollateral(to, share);\n }\n\n /// @dev Concrete implementation of `borrow`.\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\n\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\n share = bentoBox.toShare(magicInternetMoney, amount, false);\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\n\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\n }\n\n /// @notice Sender borrows `amount` and transfers it to `to`.\n /// @return part Total part of the debt held by borrowers.\n /// @return share Total amount in shares borrowed.\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\n accrue();\n (part, share) = _borrow(to, amount);\n }\n\n /// @dev Concrete implementation of `repay`.\n function _repay(\n address to,\n bool skim,\n uint256 part\n ) internal returns (uint256 amount) {\n (totalBorrow, amount) = totalBorrow.sub(part, true);\n userBorrowPart[to] = userBorrowPart[to].sub(part);\n\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\n }\n\n /// @notice Repays a loan.\n /// @param to Address of the user this payment should go.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param part The amount to repay. See `userBorrowPart`.\n /// @return amount The total amount repayed.\n function repay(\n address to,\n bool skim,\n uint256 part\n ) public returns (uint256 amount) {\n accrue();\n amount = _repay(to, skim, part);\n }\n\n // Functions that need accrue to be called\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n uint8 internal constant ACTION_BORROW = 5;\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\n uint8 internal constant ACTION_ACCRUE = 8;\n\n // Functions that don't need accrue to be called\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\n abi.decode(data, (address, bytes, bool, bool, uint8));\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\n\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\n require(success, \"Cauldron: call failed\");\n return (returnData, returnValues);\n }\n\n struct CookStatus {\n bool needsSolvencyCheck;\n bool hasAccrued;\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n CookStatus memory status;\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (!status.hasAccrued && action < 10) {\n accrue();\n status.hasAccrued = true;\n }\n if (action == ACTION_ADD_COLLATERAL) {\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n addCollateral(to, skim, _num(share, value1, value2));\n } else if (action == ACTION_REPAY) {\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n _repay(to, skim, _num(part, value1, value2));\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\n _removeCollateral(to, _num(share, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_BORROW) {\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\n (bool updated, uint256 rate) = updateExchangeRate();\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\n }\n } else if (action == ACTION_GET_REPAY_SHARE) {\n int256 part = abi.decode(datas[i], (int256));\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\n } else if (action == ACTION_GET_REPAY_PART) {\n int256 amount = abi.decode(datas[i], (int256));\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\n }\n }\n\n if (status.needsSolvencyCheck) {\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\n }\n }\n\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\n /// @param users An array of user addresses.\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\n function liquidate(\n address[] calldata users,\n uint256[] calldata maxBorrowParts,\n address to,\n ISwapper swapper\n ) public {\n // Oracle can fail but we still need to allow liquidations\n (, uint256 _exchangeRate) = updateExchangeRate();\n accrue();\n\n uint256 allCollateralShare;\n uint256 allBorrowAmount;\n uint256 allBorrowPart;\n Rebase memory _totalBorrow = totalBorrow;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n for (uint256 i = 0; i < users.length; i++) {\n address user = users[i];\n if (!_isSolvent(user, _exchangeRate)) {\n uint256 borrowPart;\n {\n uint256 availableBorrowPart = userBorrowPart[user];\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\n }\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\n uint256 collateralShare =\n bentoBoxTotals.toBase(\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\n false\n );\n\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([user,address(0)]);\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\n emit LogRemoveCollateral(user, to, collateralShare);\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\n\n // Keep totals\n allCollateralShare = allCollateralShare.add(collateralShare);\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\n allBorrowPart = allBorrowPart.add(borrowPart);\n }\n }\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\n totalBorrow = _totalBorrow;\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\n\n // Apply a percentual fee share to sSpell holders\n \n {\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\n }\n\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\n\n // Swap using a swapper freely chosen by the caller\n // Open (flash) liquidation: get proceeds first and provide the borrow after\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\n if (swapper != ISwapper(0)) {\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\n }\n\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n accrue();\n address _feeTo = masterContract.feeTo();\n uint256 _feesEarned = accrueInfo.feesEarned;\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\n accrueInfo.feesEarned = 0;\n\n emit LogWithdrawFees(_feeTo, _feesEarned);\n }\n\n /// @notice Sets the beneficiary of interest accrued.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n\n /// @notice reduces the supply of MIM\n /// @param amount amount to reduce supply by\n function reduceSupply(uint256 amount) public {\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\n }\n}\n" + }, + "contracts/interfaces/ICheckpointToken.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface ICheckpointToken {\n /// @notice checkpoint rewards for given accounts. needs to be called before any balance change\n function user_checkpoint(address[2] calldata _accounts) external returns(bool);\n}\n" + }, + "contracts/CauldronV2.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract CauldronV2 is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n CauldronV2 public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n uint64 INTEREST_PER_SECOND;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 public COLLATERIZATION_RATE;\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 public LIQUIDATION_MULTIPLIER; \r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 public BORROW_OPENING_FEE;\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n uint256 private constant DISTRIBUTION_PART = 10;\r\n uint256 private constant DISTRIBUTION_PRECISION = 100;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n // Apply a percentual fee share to sSpell holders\r\n \r\n {\r\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\r\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\r\n }\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\r\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\r\n }\r\n}\r\n" + }, + "contracts/mocks/LendingClubMock.sol": { + "content": "// SPDX-License-Identifier: MIT\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"../NFTPair.sol\";\n\n// Minimal implementation to set up some tests.\ncontract LendingClubMock {\n INFTPair private immutable nftPair;\n address private immutable investor;\n\n constructor(INFTPair _nftPair, address _investor) public {\n nftPair = _nftPair;\n investor = _investor;\n }\n\n function init() public {\n nftPair.bentoBox().setMasterContractApproval(\n address(this),\n address(nftPair.masterContract()),\n true,\n 0,\n bytes32(0),\n bytes32(0)\n );\n }\n\n function willLend(uint256 tokenId, TokenLoanParams memory requested)\n external\n view\n returns (bool)\n {\n if (msg.sender != address(nftPair)) {\n return false;\n }\n TokenLoanParams memory accepted = _lendingConditions(tokenId);\n // Valuation has to be an exact match, everything else must be at least\n // as good for the lender as `accepted`.\n\n return\n requested.valuation == accepted.valuation &&\n requested.duration <= accepted.duration &&\n requested.annualInterestBPS >= accepted.annualInterestBPS;\n }\n\n function _lendingConditions(uint256 tokenId)\n private\n pure\n returns (TokenLoanParams memory)\n {\n TokenLoanParams memory conditions;\n // No specific conditions given, but we'll take all even-numbered\n // ones at 100% APY:\n if (tokenId % 2 == 0) {\n // 256-bit addition fits by the above check.\n // Cast is.. relatively safe: this is a mock implementation,\n // production use is unlikely to follow this pattern for valuing\n // loans, and manipulating the token ID can only break the logic by\n // making the loan \"safer\" for the lender.\n conditions.valuation = uint128((tokenId + 1) * 10**18);\n conditions.duration = 365 days;\n conditions.annualInterestBPS = 10_000;\n }\n return conditions;\n }\n\n function lendingConditions(address _nftPair, uint256 tokenId)\n external\n view\n returns (TokenLoanParams memory)\n {\n if (_nftPair != address(nftPair)) {\n TokenLoanParams memory empty;\n return empty;\n } else {\n return _lendingConditions(tokenId);\n }\n }\n\n function seizeCollateral(uint256 tokenId) external {\n nftPair.removeCollateral(tokenId, investor);\n }\n\n function withdrawFunds(uint256 bentoShares) external {\n nftPair.bentoBox().transfer(\n nftPair.asset(),\n address(this),\n investor,\n bentoShares\n );\n }\n}\n" + }, + "contracts/MinimalTimeLock.sol": { + "content": "//SPDX-License-Identifier: BSD-3-Clause\n\npragma solidity ^0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\n// Modified from https://etherscan.io/address/0x6d903f6003cca6255d85cca4d3b5e5146dc33925#code and https://github.com/boringcrypto/dictator-dao/blob/main/contracts/DictatorDAO.sol#L225\ncontract MinimalTimeLock is BoringOwnable { \n event QueueTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data, uint256 eta);\n event CancelTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data);\n event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data);\n\n uint256 public constant GRACE_PERIOD = 14 days;\n uint256 public constant DELAY = 2 days;\n mapping(bytes32 => uint256) public queuedTransactions;\n\n function queueTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner returns (bytes32) {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n uint256 eta = block.timestamp + DELAY;\n queuedTransactions[txHash] = eta;\n\n emit QueueTransaction(txHash, target, value, data, eta);\n return txHash;\n }\n\n function cancelTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n queuedTransactions[txHash] = 0;\n\n emit CancelTransaction(txHash, target, value, data);\n }\n\n function executeTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner payable returns (bytes memory) {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n uint256 eta = queuedTransactions[txHash];\n require(block.timestamp >= eta, \"Too early\");\n require(block.timestamp <= eta + GRACE_PERIOD, \"Tx stale\");\n\n queuedTransactions[txHash] = 0;\n\n // solium-disable-next-line security/no-call-value\n (bool success, bytes memory returnData) = target.call{value: value}(data);\n require(success, \"Tx reverted :(\");\n\n emit ExecuteTransaction(txHash, target, value, data);\n\n return returnData;\n }\n}" + }, + "contracts/mocks/ERC20Mock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\n\ncontract ERC20Mock is ERC20 {\n uint256 public override totalSupply;\n\n constructor(uint256 _initialAmount) public {\n // Give the creator all initial tokens\n balanceOf[msg.sender] = _initialAmount;\n // Update total supply\n totalSupply = _initialAmount;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": [ + "ast" + ] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/solcInputs/6c88ba2f34cfbd7cd92d738b883e2e91.json b/deployments/ropsten/solcInputs/6c88ba2f34cfbd7cd92d738b883e2e91.json new file mode 100644 index 00000000..98751e4a --- /dev/null +++ b/deployments/ropsten/solcInputs/6c88ba2f34cfbd7cd92d738b883e2e91.json @@ -0,0 +1,35 @@ +{ + "language": "Solidity", + "sources": { + "contracts/mocks/WETH9Mock.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0-only\npragma solidity 0.6.12;\n\ncontract WETH9Mock {\n string public name = \"Wrapped Ether\";\n string public symbol = \"WETH\";\n uint8 public decimals = 18;\n\n event Approval(address indexed src, address indexed guy, uint256 wad);\n event Transfer(address indexed src, address indexed dst, uint256 wad);\n event Deposit(address indexed dst, uint256 wad);\n event Withdrawal(address indexed src, uint256 wad);\n\n mapping(address => uint256) public balanceOf;\n mapping(address => mapping(address => uint256)) public allowance;\n\n /*fallback () external payable {\n deposit();\n }*/\n function deposit() public payable {\n balanceOf[msg.sender] += msg.value;\n emit Deposit(msg.sender, msg.value);\n }\n\n function withdraw(uint256 wad) public {\n require(balanceOf[msg.sender] >= wad, \"WETH9: Error\");\n balanceOf[msg.sender] -= wad;\n msg.sender.transfer(wad);\n emit Withdrawal(msg.sender, wad);\n }\n\n function totalSupply() public view returns (uint256) {\n return address(this).balance;\n }\n\n function approve(address guy, uint256 wad) public returns (bool) {\n allowance[msg.sender][guy] = wad;\n emit Approval(msg.sender, guy, wad);\n return true;\n }\n\n function transfer(address dst, uint256 wad) public returns (bool) {\n return transferFrom(msg.sender, dst, wad);\n }\n\n function transferFrom(\n address src,\n address dst,\n uint256 wad\n ) public returns (bool) {\n require(balanceOf[src] >= wad, \"WETH9: Error\");\n\n if (src != msg.sender && allowance[src][msg.sender] != uint256(-1)) {\n require(allowance[src][msg.sender] >= wad, \"WETH9: Error\");\n allowance[src][msg.sender] -= wad;\n }\n\n balanceOf[src] -= wad;\n balanceOf[dst] += wad;\n\n emit Transfer(src, dst, wad);\n\n return true;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": [ + "ast" + ] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/solcInputs/aca01c1a6fd2699b1adc885469d39e7b.json b/deployments/ropsten/solcInputs/aca01c1a6fd2699b1adc885469d39e7b.json new file mode 100644 index 00000000..c0cdd2ab --- /dev/null +++ b/deployments/ropsten/solcInputs/aca01c1a6fd2699b1adc885469d39e7b.json @@ -0,0 +1,38 @@ +{ + "language": "Solidity", + "sources": { + "contracts/BentoBoxFlat.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n// The BentoBox\n\n// ▄▄▄▄· ▄▄▄ . ▐ ▄ ▄▄▄▄▄ ▄▄▄▄· ▐▄• ▄\n// ▐█ ▀█▪▀▄.▀·█▌▐█•██ ▪ ▐█ ▀█▪▪ █▌█▌▪\n// ▐█▀▀█▄▐▀▀▪▄▐█▐▐▌ ▐█.▪ ▄█▀▄ ▐█▀▀█▄ ▄█▀▄ ·██·\n// ██▄▪▐█▐█▄▄▌██▐█▌ ▐█▌·▐█▌.▐▌██▄▪▐█▐█▌.▐▌▪▐█·█▌\n// ·▀▀▀▀ ▀▀▀ ▀▀ █▪ ▀▀▀ ▀█▄▀▪·▀▀▀▀ ▀█▄▀▪•▀▀ ▀▀\n\n// This contract stores funds, handles their transfers, supports flash loans and strategies.\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to Keno for all his hard work and support\n\n// Version 22-Mar-2021\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\n// solhint-disable avoid-low-level-calls\n// solhint-disable not-rely-on-time\n// solhint-disable no-inline-assembly\n\n// File @boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol@v1.2.0\n// License-Identifier: MIT\n\ninterface IERC20 {\n function totalSupply() external view returns (uint256);\n\n function balanceOf(address account) external view returns (uint256);\n\n function allowance(address owner, address spender) external view returns (uint256);\n\n function approve(address spender, uint256 amount) external returns (bool);\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /// @notice EIP 2612\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n}\n\n// File contracts/interfaces/IFlashLoan.sol\n// License-Identifier: MIT\n\ninterface IFlashBorrower {\n /// @notice The flashloan callback. `amount` + `fee` needs to repayed to msg.sender before this call returns.\n /// @param sender The address of the invoker of this flashloan.\n /// @param token The address of the token that is loaned.\n /// @param amount of the `token` that is loaned.\n /// @param fee The fee that needs to be paid on top for this loan. Needs to be the same as `token`.\n /// @param data Additional data that was passed to the flashloan function.\n function onFlashLoan(\n address sender,\n IERC20 token,\n uint256 amount,\n uint256 fee,\n bytes calldata data\n ) external;\n}\n\ninterface IBatchFlashBorrower {\n /// @notice The callback for batched flashloans. Every amount + fee needs to repayed to msg.sender before this call returns.\n /// @param sender The address of the invoker of this flashloan.\n /// @param tokens Array of addresses for ERC-20 tokens that is loaned.\n /// @param amounts A one-to-one map to `tokens` that is loaned.\n /// @param fees A one-to-one map to `tokens` that needs to be paid on top for each loan. Needs to be the same token.\n /// @param data Additional data that was passed to the flashloan function.\n function onBatchFlashLoan(\n address sender,\n IERC20[] calldata tokens,\n uint256[] calldata amounts,\n uint256[] calldata fees,\n bytes calldata data\n ) external;\n}\n\n// File contracts/interfaces/IWETH.sol\n// License-Identifier: MIT\n\ninterface IWETH {\n function deposit() external payable;\n\n function withdraw(uint256) external;\n}\n\n// File contracts/interfaces/IStrategy.sol\n// License-Identifier: MIT\n\ninterface IStrategy {\n /// @notice Send the assets to the Strategy and call skim to invest them.\n /// @param amount The amount of tokens to invest.\n function skim(uint256 amount) external;\n\n /// @notice Harvest any profits made converted to the asset and pass them to the caller.\n /// @param balance The amount of tokens the caller thinks it has invested.\n /// @param sender The address of the initiator of this transaction. Can be used for reimbursements, etc.\n /// @return amountAdded The delta (+profit or -loss) that occured in contrast to `balance`.\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\n\n /// @notice Withdraw assets. The returned amount can differ from the requested amount due to rounding.\n /// @dev The `actualAmount` should be very close to the amount.\n /// The difference should NOT be used to report a loss. That's what harvest is for.\n /// @param amount The requested amount the caller wants to withdraw.\n /// @return actualAmount The real amount that is withdrawn.\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\n\n /// @notice Withdraw all assets in the safest way possible. This shouldn't fail.\n /// @param balance The amount of tokens the caller thinks it has invested.\n /// @return amountAdded The delta (+profit or -loss) that occured in contrast to `balance`.\n function exit(uint256 balance) external returns (int256 amountAdded);\n}\n\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol@v1.2.0\n// License-Identifier: MIT\n\nlibrary BoringERC20 {\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\n\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransfer(\n IERC20 token,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: Transfer failed\");\n }\n\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param from Transfer tokens from.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransferFrom(\n IERC20 token,\n address from,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: TransferFrom failed\");\n }\n}\n\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol@v1.2.0\n// License-Identifier: MIT\n\n/// @notice A library for performing overflow-/underflow-safe math,\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\nlibrary BoringMath {\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require(b == 0 || (c = a * b) / b == a, \"BoringMath: Mul Overflow\");\n }\n\n function to128(uint256 a) internal pure returns (uint128 c) {\n require(a <= uint128(-1), \"BoringMath: uint128 Overflow\");\n c = uint128(a);\n }\n\n function to64(uint256 a) internal pure returns (uint64 c) {\n require(a <= uint64(-1), \"BoringMath: uint64 Overflow\");\n c = uint64(a);\n }\n\n function to32(uint256 a) internal pure returns (uint32 c) {\n require(a <= uint32(-1), \"BoringMath: uint32 Overflow\");\n c = uint32(a);\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\nlibrary BoringMath128 {\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\nlibrary BoringMath64 {\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\nlibrary BoringMath32 {\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n// File @boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol@v1.2.0\n// License-Identifier: MIT\n\nstruct Rebase {\n uint128 elastic;\n uint128 base;\n}\n\n/// @notice A rebasing library using overflow-/underflow-safe math.\nlibrary RebaseLibrary {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\n function toBase(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (uint256 base) {\n if (total.elastic == 0) {\n base = elastic;\n } else {\n base = elastic.mul(total.base) / total.elastic;\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\n base = base.add(1);\n }\n }\n }\n\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\n function toElastic(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (uint256 elastic) {\n if (total.base == 0) {\n elastic = base;\n } else {\n elastic = base.mul(total.elastic) / total.base;\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\n elastic = elastic.add(1);\n }\n }\n }\n\n /// @notice Add `elastic` to `total` and doubles `total.base`.\n /// @return (Rebase) The new total.\n /// @return base in relationship to `elastic`.\n function add(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 base) {\n base = toBase(total, elastic, roundUp);\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return (total, base);\n }\n\n /// @notice Sub `base` from `total` and update `total.elastic`.\n /// @return (Rebase) The new total.\n /// @return elastic in relationship to `base`.\n function sub(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 elastic) {\n elastic = toElastic(total, base, roundUp);\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return (total, elastic);\n }\n\n /// @notice Add `elastic` and `base` to `total`.\n function add(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return total;\n }\n\n /// @notice Subtract `elastic` and `base` to `total`.\n function sub(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return total;\n }\n\n /// @notice Add `elastic` to `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.add(elastic.to128());\n }\n\n /// @notice Subtract `elastic` from `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\n }\n}\n\n// File @boringcrypto/boring-solidity/contracts/BoringOwnable.sol@v1.2.0\n// License-Identifier: MIT\n\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\n// Edited by BoringCrypto\n\ncontract BoringOwnableData {\n address public owner;\n address public pendingOwner;\n}\n\ncontract BoringOwnable is BoringOwnableData {\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /// @notice `owner` defaults to msg.sender on construction.\n constructor() public {\n owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\n /// Can only be invoked by the current `owner`.\n /// @param newOwner Address of the new owner.\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\n function transferOwnership(\n address newOwner,\n bool direct,\n bool renounce\n ) public onlyOwner {\n if (direct) {\n // Checks\n require(newOwner != address(0) || renounce, \"Ownable: zero address\");\n\n // Effects\n emit OwnershipTransferred(owner, newOwner);\n owner = newOwner;\n pendingOwner = address(0);\n } else {\n // Effects\n pendingOwner = newOwner;\n }\n }\n\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\n function claimOwnership() public {\n address _pendingOwner = pendingOwner;\n\n // Checks\n require(msg.sender == _pendingOwner, \"Ownable: caller != pending owner\");\n\n // Effects\n emit OwnershipTransferred(owner, _pendingOwner);\n owner = _pendingOwner;\n pendingOwner = address(0);\n }\n\n /// @notice Only allows the `owner` to execute the function.\n modifier onlyOwner() {\n require(msg.sender == owner, \"Ownable: caller is not the owner\");\n _;\n }\n}\n\n// File @boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol@v1.2.0\n// License-Identifier: MIT\n\ninterface IMasterContract {\n /// @notice Init function that gets called from `BoringFactory.deploy`.\n /// Also kown as the constructor for cloned contracts.\n /// Any ETH send to `BoringFactory.deploy` ends up here.\n /// @param data Can be abi encoded arguments or anything else.\n function init(bytes calldata data) external payable;\n}\n\n// File @boringcrypto/boring-solidity/contracts/BoringFactory.sol@v1.2.0\n// License-Identifier: MIT\n\ncontract BoringFactory {\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\n\n /// @notice Mapping from clone contracts to their masterContract.\n mapping(address => address) public masterContractOf;\n\n /// @notice Deploys a given master Contract as a clone.\n /// Any ETH transferred with this call is forwarded to the new clone.\n /// Emits `LogDeploy`.\n /// @param masterContract The address of the contract to clone.\n /// @param data Additional abi encoded calldata that is passed to the new clone via `IMasterContract.init`.\n /// @param useCreate2 Creates the clone by using the CREATE2 opcode, in this case `data` will be used as salt.\n /// @return cloneAddress Address of the created clone contract.\n function deploy(\n address masterContract,\n bytes calldata data,\n bool useCreate2\n ) public payable returns (address cloneAddress) {\n require(masterContract != address(0), \"BoringFactory: No masterContract\");\n bytes20 targetBytes = bytes20(masterContract); // Takes the first 20 bytes of the masterContract's address\n\n if (useCreate2) {\n // each masterContract has different code already. So clones are distinguished by their data only.\n bytes32 salt = keccak256(data);\n\n // Creates clone, more info here: https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/\n assembly {\n let clone := mload(0x40)\n mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)\n mstore(add(clone, 0x14), targetBytes)\n mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)\n cloneAddress := create2(0, clone, 0x37, salt)\n }\n } else {\n assembly {\n let clone := mload(0x40)\n mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)\n mstore(add(clone, 0x14), targetBytes)\n mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)\n cloneAddress := create(0, clone, 0x37)\n }\n }\n masterContractOf[cloneAddress] = masterContract;\n\n IMasterContract(cloneAddress).init{value: msg.value}(data);\n\n emit LogDeploy(masterContract, data, cloneAddress);\n }\n}\n\n// File contracts/MasterContractManager.sol\n// License-Identifier: UNLICENSED\n\ncontract MasterContractManager is BoringOwnable, BoringFactory {\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\n event LogRegisterProtocol(address indexed protocol);\n\n /// @notice masterContract to user to approval state\n mapping(address => mapping(address => bool)) public masterContractApproved;\n /// @notice masterContract to whitelisted state for approval without signed message\n mapping(address => bool) public whitelistedMasterContracts;\n /// @notice user nonces for masterContract approvals\n mapping(address => uint256) public nonces;\n\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\"EIP712Domain(string name,uint256 chainId,address verifyingContract)\");\n // See https://eips.ethereum.org/EIPS/eip-191\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \"\\x19\\x01\";\n bytes32 private constant APPROVAL_SIGNATURE_HASH =\n keccak256(\"SetMasterContractApproval(string warning,address user,address masterContract,bool approved,uint256 nonce)\");\n\n // solhint-disable-next-line var-name-mixedcase\n bytes32 private immutable _DOMAIN_SEPARATOR;\n // solhint-disable-next-line var-name-mixedcase\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\n\n constructor() public {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\n }\n\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, keccak256(\"BentoBox V1\"), chainId, address(this)));\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() public view returns (bytes32) {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\n }\n\n /// @notice Other contracts need to register with this master contract so that users can approve them for the BentoBox.\n function registerProtocol() public {\n masterContractOf[msg.sender] = msg.sender;\n emit LogRegisterProtocol(msg.sender);\n }\n\n /// @notice Enables or disables a contract for approval without signed message.\n function whitelistMasterContract(address masterContract, bool approved) public onlyOwner {\n // Checks\n require(masterContract != address(0), \"MasterCMgr: Cannot approve 0\");\n\n // Effects\n whitelistedMasterContracts[masterContract] = approved;\n emit LogWhiteListMasterContract(masterContract, approved);\n }\n\n /// @notice Approves or revokes a `masterContract` access to `user` funds.\n /// @param user The address of the user that approves or revokes access.\n /// @param masterContract The address who gains or loses access.\n /// @param approved If True approves access. If False revokes access.\n /// @param v Part of the signature. (See EIP-191)\n /// @param r Part of the signature. (See EIP-191)\n /// @param s Part of the signature. (See EIP-191)\n // F4 - Check behaviour for all function arguments when wrong or extreme\n // F4: Don't allow masterContract 0 to be approved. Unknown contracts will have a masterContract of 0.\n // F4: User can't be 0 for signed approvals because the recoveredAddress will be 0 if ecrecover fails\n function setMasterContractApproval(\n address user,\n address masterContract,\n bool approved,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n // Checks\n require(masterContract != address(0), \"MasterCMgr: masterC not set\"); // Important for security\n\n // If no signature is provided, the fallback is executed\n if (r == 0 && s == 0 && v == 0) {\n require(user == msg.sender, \"MasterCMgr: user not sender\");\n require(masterContractOf[user] == address(0), \"MasterCMgr: user is clone\");\n require(whitelistedMasterContracts[masterContract], \"MasterCMgr: not whitelisted\");\n } else {\n // Important for security - any address without masterContract has address(0) as masterContract\n // So approving address(0) would approve every address, leading to full loss of funds\n // Also, ecrecover returns address(0) on failure. So we check this:\n require(user != address(0), \"MasterCMgr: User cannot be 0\");\n\n // C10 - Protect signatures against replay, use nonce and chainId (SWC-121)\n // C10: nonce + chainId are used to prevent replays\n // C11 - All signatures strictly EIP-712 (SWC-117 SWC-122)\n // C11: signature is EIP-712 compliant\n // C12 - abi.encodePacked can't contain variable length user input (SWC-133)\n // C12: abi.encodePacked has fixed length parameters\n bytes32 digest =\n keccak256(\n abi.encodePacked(\n EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA,\n DOMAIN_SEPARATOR(),\n keccak256(\n abi.encode(\n APPROVAL_SIGNATURE_HASH,\n approved\n ? keccak256(\"Give FULL access to funds in (and approved to) BentoBox?\")\n : keccak256(\"Revoke access to BentoBox?\"),\n user,\n masterContract,\n approved,\n nonces[user]++\n )\n )\n )\n );\n address recoveredAddress = ecrecover(digest, v, r, s);\n require(recoveredAddress == user, \"MasterCMgr: Invalid Signature\");\n }\n\n // Effects\n masterContractApproved[masterContract][user] = approved;\n emit LogSetMasterContractApproval(masterContract, user, approved);\n }\n}\n\n// File @boringcrypto/boring-solidity/contracts/BoringBatchable.sol@v1.2.0\n// License-Identifier: MIT\n\ncontract BaseBoringBatchable {\n /// @dev Helper function to extract a useful revert message from a failed call.\n /// If the returned data is malformed or not correctly abi encoded then this call can fail itself.\n function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {\n // If the _res length is less than 68, then the transaction failed silently (without a revert message)\n if (_returnData.length < 68) return \"Transaction reverted silently\";\n\n assembly {\n // Slice the sighash.\n _returnData := add(_returnData, 0x04)\n }\n return abi.decode(_returnData, (string)); // All that remains is the revert string\n }\n\n /// @notice Allows batched call to self (this contract).\n /// @param calls An array of inputs for each call.\n /// @param revertOnFail If True then reverts after a failed call and stops doing further calls.\n /// @return successes An array indicating the success of a call, mapped one-to-one to `calls`.\n /// @return results An array with the returned data of each function call, mapped one-to-one to `calls`.\n // F1: External is ok here because this is the batch function, adding it to a batch makes no sense\n // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value\n // C3: The length of the loop is fully under user control, so can't be exploited\n // C7: Delegatecall is only used on the same contract, so it's safe\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results) {\n successes = new bool[](calls.length);\n results = new bytes[](calls.length);\n for (uint256 i = 0; i < calls.length; i++) {\n (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\n require(success || !revertOnFail, _getRevertMsg(result));\n successes[i] = success;\n results[i] = result;\n }\n }\n}\n\ncontract BoringBatchable is BaseBoringBatchable {\n /// @notice Call wrapper that performs `ERC20.permit` on `token`.\n /// Lookup `IERC20.permit`.\n // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert)\n // if part of a batch this could be used to grief once as the second call would not need the permit\n function permitToken(\n IERC20 token,\n address from,\n address to,\n uint256 amount,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n token.permit(from, to, amount, deadline, v, r, s);\n }\n}\n\n// File contracts/BentoBox.sol\n// License-Identifier: UNLICENSED\n\n/// @title BentoBox\n/// @author BoringCrypto, Keno\n/// @notice The BentoBox is a vault for tokens. The stored tokens can be flash loaned and used in strategies.\n/// Yield from this will go to the token depositors.\n/// Rebasing tokens ARE NOT supported and WILL cause loss of funds.\n/// Any funds transfered directly onto the BentoBox will be lost, use the deposit function instead.\ncontract BentoBoxV1 is MasterContractManager, BoringBatchable {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using BoringERC20 for IERC20;\n using RebaseLibrary for Rebase;\n\n // ************** //\n // *** EVENTS *** //\n // ************** //\n\n event LogDeposit(IERC20 indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event LogWithdraw(IERC20 indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event LogTransfer(IERC20 indexed token, address indexed from, address indexed to, uint256 share);\n\n event LogFlashLoan(address indexed borrower, IERC20 indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\n\n event LogStrategyTargetPercentage(IERC20 indexed token, uint256 targetPercentage);\n event LogStrategyQueued(IERC20 indexed token, IStrategy indexed strategy);\n event LogStrategySet(IERC20 indexed token, IStrategy indexed strategy);\n event LogStrategyInvest(IERC20 indexed token, uint256 amount);\n event LogStrategyDivest(IERC20 indexed token, uint256 amount);\n event LogStrategyProfit(IERC20 indexed token, uint256 amount);\n event LogStrategyLoss(IERC20 indexed token, uint256 amount);\n\n // *************** //\n // *** STRUCTS *** //\n // *************** //\n\n struct StrategyData {\n uint64 strategyStartDate;\n uint64 targetPercentage;\n uint128 balance; // the balance of the strategy that BentoBox thinks is in there\n }\n\n // ******************************** //\n // *** CONSTANTS AND IMMUTABLES *** //\n // ******************************** //\n\n // V2 - Can they be private?\n // V2: Private to save gas, to verify it's correct, check the constructor arguments\n IERC20 private immutable wethToken;\n\n IERC20 private constant USE_ETHEREUM = IERC20(0);\n uint256 private constant FLASH_LOAN_FEE = 50; // 0.05%\n uint256 private constant FLASH_LOAN_FEE_PRECISION = 1e5;\n uint256 private constant STRATEGY_DELAY = 2 weeks;\n uint256 private constant MAX_TARGET_PERCENTAGE = 95; // 95%\n uint256 private constant MINIMUM_SHARE_BALANCE = 1000; // To prevent the ratio going off\n\n // ***************** //\n // *** VARIABLES *** //\n // ***************** //\n\n // Balance per token per address/contract in shares\n mapping(IERC20 => mapping(address => uint256)) public balanceOf;\n\n // Rebase from amount to share\n mapping(IERC20 => Rebase) public totals;\n\n mapping(IERC20 => IStrategy) public strategy;\n mapping(IERC20 => IStrategy) public pendingStrategy;\n mapping(IERC20 => StrategyData) public strategyData;\n\n // ******************* //\n // *** CONSTRUCTOR *** //\n // ******************* //\n\n constructor(IERC20 wethToken_) public {\n wethToken = wethToken_;\n }\n\n // ***************** //\n // *** MODIFIERS *** //\n // ***************** //\n\n /// Modifier to check if the msg.sender is allowed to use funds belonging to the 'from' address.\n /// If 'from' is msg.sender, it's allowed.\n /// If 'from' is the BentoBox itself, it's allowed. Any ETH, token balances (above the known balances) or BentoBox balances\n /// can be taken by anyone.\n /// This is to enable skimming, not just for deposits, but also for withdrawals or transfers, enabling better composability.\n /// If 'from' is a clone of a masterContract AND the 'from' address has approved that masterContract, it's allowed.\n modifier allowed(address from) {\n if (from != msg.sender && from != address(this)) {\n // From is sender or you are skimming\n address masterContract = masterContractOf[msg.sender];\n require(masterContract != address(0), \"BentoBox: no masterContract\");\n require(masterContractApproved[masterContract][from], \"BentoBox: Transfer not approved\");\n }\n _;\n }\n\n // ************************** //\n // *** INTERNAL FUNCTIONS *** //\n // ************************** //\n\n /// @dev Returns the total balance of `token` this contracts holds,\n /// plus the total amount this contract thinks the strategy holds.\n function _tokenBalanceOf(IERC20 token) internal view returns (uint256 amount) {\n amount = token.balanceOf(address(this)).add(strategyData[token].balance);\n }\n\n // ************************ //\n // *** PUBLIC FUNCTIONS *** //\n // ************************ //\n\n /// @dev Helper function to represent an `amount` of `token` in shares.\n /// @param token The ERC-20 token.\n /// @param amount The `token` amount.\n /// @param roundUp If the result `share` should be rounded up.\n /// @return share The token amount represented in shares.\n function toShare(\n IERC20 token,\n uint256 amount,\n bool roundUp\n ) external view returns (uint256 share) {\n share = totals[token].toBase(amount, roundUp);\n }\n\n /// @dev Helper function represent shares back into the `token` amount.\n /// @param token The ERC-20 token.\n /// @param share The amount of shares.\n /// @param roundUp If the result should be rounded up.\n /// @return amount The share amount back into native representation.\n function toAmount(\n IERC20 token,\n uint256 share,\n bool roundUp\n ) external view returns (uint256 amount) {\n amount = totals[token].toElastic(share, roundUp);\n }\n\n /// @notice Deposit an amount of `token` represented in either `amount` or `share`.\n /// @param token_ The ERC-20 token to deposit.\n /// @param from which account to pull the tokens.\n /// @param to which account to push the tokens.\n /// @param amount Token amount in native representation to deposit.\n /// @param share Token amount represented in shares to deposit. Takes precedence over `amount`.\n /// @return amountOut The amount deposited.\n /// @return shareOut The deposited amount repesented in shares.\n function deposit(\n IERC20 token_,\n address from,\n address to,\n uint256 amount,\n uint256 share\n ) public payable allowed(from) returns (uint256 amountOut, uint256 shareOut) {\n // Checks\n require(to != address(0), \"BentoBox: to not set\"); // To avoid a bad UI from burning funds\n\n // Effects\n IERC20 token = token_ == USE_ETHEREUM ? wethToken : token_;\n Rebase memory total = totals[token];\n\n // If a new token gets added, the tokenSupply call checks that this is a deployed contract. Needed for security.\n require(total.elastic != 0 || token.totalSupply() > 0, \"BentoBox: No tokens\");\n if (share == 0) {\n // value of the share may be lower than the amount due to rounding, that's ok\n share = total.toBase(amount, false);\n // Any deposit should lead to at least the minimum share balance, otherwise it's ignored (no amount taken)\n if (total.base.add(share.to128()) < MINIMUM_SHARE_BALANCE) {\n return (0, 0);\n }\n } else {\n // amount may be lower than the value of share due to rounding, in that case, add 1 to amount (Always round up)\n amount = total.toElastic(share, true);\n }\n\n // In case of skimming, check that only the skimmable amount is taken.\n // For ETH, the full balance is available, so no need to check.\n // During flashloans the _tokenBalanceOf is lower than 'reality', so skimming deposits will mostly fail during a flashloan.\n require(\n from != address(this) || token_ == USE_ETHEREUM || amount <= _tokenBalanceOf(token).sub(total.elastic),\n \"BentoBox: Skim too much\"\n );\n\n balanceOf[token][to] = balanceOf[token][to].add(share);\n total.base = total.base.add(share.to128());\n total.elastic = total.elastic.add(amount.to128());\n totals[token] = total;\n\n // Interactions\n // During the first deposit, we check that this token is 'real'\n if (token_ == USE_ETHEREUM) {\n // X2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113)\n // X2: If the WETH implementation is faulty or malicious, it will block adding ETH (but we know the WETH implementation)\n IWETH(address(wethToken)).deposit{value: amount}();\n } else if (from != address(this)) {\n // X2 - If there is an error, could it cause a DoS. Like balanceOf causing revert. (SWC-113)\n // X2: If the token implementation is faulty or malicious, it may block adding tokens. Good.\n token.safeTransferFrom(from, address(this), amount);\n }\n emit LogDeposit(token, from, to, amount, share);\n amountOut = amount;\n shareOut = share;\n }\n\n /// @notice Withdraws an amount of `token` from a user account.\n /// @param token_ The ERC-20 token to withdraw.\n /// @param from which user to pull the tokens.\n /// @param to which user to push the tokens.\n /// @param amount of tokens. Either one of `amount` or `share` needs to be supplied.\n /// @param share Like above, but `share` takes precedence over `amount`.\n function withdraw(\n IERC20 token_,\n address from,\n address to,\n uint256 amount,\n uint256 share\n ) public allowed(from) returns (uint256 amountOut, uint256 shareOut) {\n // Checks\n require(to != address(0), \"BentoBox: to not set\"); // To avoid a bad UI from burning funds\n\n // Effects\n IERC20 token = token_ == USE_ETHEREUM ? wethToken : token_;\n Rebase memory total = totals[token];\n if (share == 0) {\n // value of the share paid could be lower than the amount paid due to rounding, in that case, add a share (Always round up)\n share = total.toBase(amount, true);\n } else {\n // amount may be lower than the value of share due to rounding, that's ok\n amount = total.toElastic(share, false);\n }\n\n balanceOf[token][from] = balanceOf[token][from].sub(share);\n total.elastic = total.elastic.sub(amount.to128());\n total.base = total.base.sub(share.to128());\n // There have to be at least 1000 shares left to prevent reseting the share/amount ratio (unless it's fully emptied)\n require(total.base >= MINIMUM_SHARE_BALANCE || total.base == 0, \"BentoBox: cannot empty\");\n totals[token] = total;\n\n // Interactions\n if (token_ == USE_ETHEREUM) {\n // X2, X3: A revert or big gas usage in the WETH contract could block withdrawals, but WETH9 is fine.\n IWETH(address(wethToken)).withdraw(amount);\n // X2, X3: A revert or big gas usage could block, however, the to address is under control of the caller.\n (bool success, ) = to.call{value: amount}(\"\");\n require(success, \"BentoBox: ETH transfer failed\");\n } else {\n // X2, X3: A malicious token could block withdrawal of just THAT token.\n // masterContracts may want to take care not to rely on withdraw always succeeding.\n token.safeTransfer(to, amount);\n }\n emit LogWithdraw(token, from, to, amount, share);\n amountOut = amount;\n shareOut = share;\n }\n\n /// @notice Transfer shares from a user account to another one.\n /// @param token The ERC-20 token to transfer.\n /// @param from which user to pull the tokens.\n /// @param to which user to push the tokens.\n /// @param share The amount of `token` in shares.\n // Clones of master contracts can transfer from any account that has approved them\n // F3 - Can it be combined with another similar function?\n // F3: This isn't combined with transferMultiple for gas optimization\n function transfer(\n IERC20 token,\n address from,\n address to,\n uint256 share\n ) public allowed(from) {\n // Checks\n require(to != address(0), \"BentoBox: to not set\"); // To avoid a bad UI from burning funds\n\n // Effects\n balanceOf[token][from] = balanceOf[token][from].sub(share);\n balanceOf[token][to] = balanceOf[token][to].add(share);\n\n emit LogTransfer(token, from, to, share);\n }\n\n /// @notice Transfer shares from a user account to multiple other ones.\n /// @param token The ERC-20 token to transfer.\n /// @param from which user to pull the tokens.\n /// @param tos The receivers of the tokens.\n /// @param shares The amount of `token` in shares for each receiver in `tos`.\n // F3 - Can it be combined with another similar function?\n // F3: This isn't combined with transfer for gas optimization\n function transferMultiple(\n IERC20 token,\n address from,\n address[] calldata tos,\n uint256[] calldata shares\n ) public allowed(from) {\n // Checks\n require(tos[0] != address(0), \"BentoBox: to[0] not set\"); // To avoid a bad UI from burning funds\n\n // Effects\n uint256 totalAmount;\n uint256 len = tos.length;\n for (uint256 i = 0; i < len; i++) {\n address to = tos[i];\n balanceOf[token][to] = balanceOf[token][to].add(shares[i]);\n totalAmount = totalAmount.add(shares[i]);\n emit LogTransfer(token, from, to, shares[i]);\n }\n balanceOf[token][from] = balanceOf[token][from].sub(totalAmount);\n }\n\n /// @notice Flashloan ability.\n /// @param borrower The address of the contract that implements and conforms to `IFlashBorrower` and handles the flashloan.\n /// @param receiver Address of the token receiver.\n /// @param token The address of the token to receive.\n /// @param amount of the tokens to receive.\n /// @param data The calldata to pass to the `borrower` contract.\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\n // F5: Not possible to follow this here, reentrancy has been reviewed\n // F6 - Check for front-running possibilities, such as the approve function (SWC-114)\n // F6: Slight grieving possible by withdrawing an amount before someone tries to flashloan close to the full amount.\n function flashLoan(\n IFlashBorrower borrower,\n address receiver,\n IERC20 token,\n uint256 amount,\n bytes calldata data\n ) public {\n uint256 fee = amount.mul(FLASH_LOAN_FEE) / FLASH_LOAN_FEE_PRECISION;\n token.safeTransfer(receiver, amount);\n\n borrower.onFlashLoan(msg.sender, token, amount, fee, data);\n\n require(_tokenBalanceOf(token) >= totals[token].addElastic(fee.to128()), \"BentoBox: Wrong amount\");\n emit LogFlashLoan(address(borrower), token, amount, fee, receiver);\n }\n\n /// @notice Support for batched flashloans. Useful to request multiple different `tokens` in a single transaction.\n /// @param borrower The address of the contract that implements and conforms to `IBatchFlashBorrower` and handles the flashloan.\n /// @param receivers An array of the token receivers. A one-to-one mapping with `tokens` and `amounts`.\n /// @param tokens The addresses of the tokens.\n /// @param amounts of the tokens for each receiver.\n /// @param data The calldata to pass to the `borrower` contract.\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\n // F5: Not possible to follow this here, reentrancy has been reviewed\n // F6 - Check for front-running possibilities, such as the approve function (SWC-114)\n // F6: Slight grieving possible by withdrawing an amount before someone tries to flashloan close to the full amount.\n function batchFlashLoan(\n IBatchFlashBorrower borrower,\n address[] calldata receivers,\n IERC20[] calldata tokens,\n uint256[] calldata amounts,\n bytes calldata data\n ) public {\n uint256[] memory fees = new uint256[](tokens.length);\n\n uint256 len = tokens.length;\n for (uint256 i = 0; i < len; i++) {\n uint256 amount = amounts[i];\n fees[i] = amount.mul(FLASH_LOAN_FEE) / FLASH_LOAN_FEE_PRECISION;\n\n tokens[i].safeTransfer(receivers[i], amounts[i]);\n }\n\n borrower.onBatchFlashLoan(msg.sender, tokens, amounts, fees, data);\n\n for (uint256 i = 0; i < len; i++) {\n IERC20 token = tokens[i];\n require(_tokenBalanceOf(token) >= totals[token].addElastic(fees[i].to128()), \"BentoBox: Wrong amount\");\n emit LogFlashLoan(address(borrower), token, amounts[i], fees[i], receivers[i]);\n }\n }\n\n /// @notice Sets the target percentage of the strategy for `token`.\n /// @dev Only the owner of this contract is allowed to change this.\n /// @param token The address of the token that maps to a strategy to change.\n /// @param targetPercentage_ The new target in percent. Must be lesser or equal to `MAX_TARGET_PERCENTAGE`.\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) public onlyOwner {\n // Checks\n require(targetPercentage_ <= MAX_TARGET_PERCENTAGE, \"StrategyManager: Target too high\");\n\n // Effects\n strategyData[token].targetPercentage = targetPercentage_;\n emit LogStrategyTargetPercentage(token, targetPercentage_);\n }\n\n /// @notice Sets the contract address of a new strategy that conforms to `IStrategy` for `token`.\n /// Must be called twice with the same arguments.\n /// A new strategy becomes pending first and can be activated once `STRATEGY_DELAY` is over.\n /// @dev Only the owner of this contract is allowed to change this.\n /// @param token The address of the token that maps to a strategy to change.\n /// @param newStrategy The address of the contract that conforms to `IStrategy`.\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\n // F5: Total amount is updated AFTER interaction. But strategy is under our control.\n // C4 - Use block.timestamp only for long intervals (SWC-116)\n // C4: block.timestamp is used for a period of 2 weeks, which is long enough\n function setStrategy(IERC20 token, IStrategy newStrategy) public onlyOwner {\n StrategyData memory data = strategyData[token];\n IStrategy pending = pendingStrategy[token];\n if (data.strategyStartDate == 0 || pending != newStrategy) {\n pendingStrategy[token] = newStrategy;\n // C1 - All math done through BoringMath (SWC-101)\n // C1: Our sun will swallow the earth well before this overflows\n data.strategyStartDate = (block.timestamp + STRATEGY_DELAY).to64();\n emit LogStrategyQueued(token, newStrategy);\n } else {\n require(data.strategyStartDate != 0 && block.timestamp >= data.strategyStartDate, \"StrategyManager: Too early\");\n if (address(strategy[token]) != address(0)) {\n int256 balanceChange = strategy[token].exit(data.balance);\n // Effects\n if (balanceChange > 0) {\n uint256 add = uint256(balanceChange);\n totals[token].addElastic(add);\n emit LogStrategyProfit(token, add);\n } else if (balanceChange < 0) {\n uint256 sub = uint256(-balanceChange);\n totals[token].subElastic(sub);\n emit LogStrategyLoss(token, sub);\n }\n\n emit LogStrategyDivest(token, data.balance);\n }\n strategy[token] = pending;\n data.strategyStartDate = 0;\n data.balance = 0;\n pendingStrategy[token] = IStrategy(0);\n emit LogStrategySet(token, newStrategy);\n }\n strategyData[token] = data;\n }\n\n /// @notice The actual process of yield farming. Executes the strategy of `token`.\n /// Optionally does housekeeping if `balance` is true.\n /// `maxChangeAmount` is relevant for skimming or withdrawing if `balance` is true.\n /// @param token The address of the token for which a strategy is deployed.\n /// @param balance True if housekeeping should be done.\n /// @param maxChangeAmount The maximum amount for either pulling or pushing from/to the `IStrategy` contract.\n // F5 - Checks-Effects-Interactions pattern followed? (SWC-107)\n // F5: Total amount is updated AFTER interaction. But strategy is under our control.\n // F5: Not followed to prevent reentrancy issues with flashloans and BentoBox skims?\n function harvest(\n IERC20 token,\n bool balance,\n uint256 maxChangeAmount\n ) public {\n StrategyData memory data = strategyData[token];\n IStrategy _strategy = strategy[token];\n int256 balanceChange = _strategy.harvest(data.balance, msg.sender);\n if (balanceChange == 0 && !balance) {\n return;\n }\n\n uint256 totalElastic = totals[token].elastic;\n\n if (balanceChange > 0) {\n uint256 add = uint256(balanceChange);\n totalElastic = totalElastic.add(add);\n totals[token].elastic = totalElastic.to128();\n emit LogStrategyProfit(token, add);\n } else if (balanceChange < 0) {\n // C1 - All math done through BoringMath (SWC-101)\n // C1: balanceChange could overflow if it's max negative int128.\n // But tokens with balances that large are not supported by the BentoBox.\n uint256 sub = uint256(-balanceChange);\n totalElastic = totalElastic.sub(sub);\n totals[token].elastic = totalElastic.to128();\n data.balance = data.balance.sub(sub.to128());\n emit LogStrategyLoss(token, sub);\n }\n\n if (balance) {\n uint256 targetBalance = totalElastic.mul(data.targetPercentage) / 100;\n // if data.balance == targetBalance there is nothing to update\n if (data.balance < targetBalance) {\n uint256 amountOut = targetBalance.sub(data.balance);\n if (maxChangeAmount != 0 && amountOut > maxChangeAmount) {\n amountOut = maxChangeAmount;\n }\n token.safeTransfer(address(_strategy), amountOut);\n data.balance = data.balance.add(amountOut.to128());\n _strategy.skim(amountOut);\n emit LogStrategyInvest(token, amountOut);\n } else if (data.balance > targetBalance) {\n uint256 amountIn = data.balance.sub(targetBalance.to128());\n if (maxChangeAmount != 0 && amountIn > maxChangeAmount) {\n amountIn = maxChangeAmount;\n }\n\n uint256 actualAmountIn = _strategy.withdraw(amountIn);\n\n data.balance = data.balance.sub(actualAmountIn.to128());\n emit LogStrategyDivest(token, actualAmountIn);\n }\n }\n\n strategyData[token] = data;\n }\n\n // Contract should be able to receive ETH deposits to support deposit & skim\n // solhint-disable-next-line no-empty-blocks\n receive() external payable {}\n}\n" + }, + "contracts/mocks/BentoBoxMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../BentoBoxFlat.sol\";\n\ncontract BentoBoxMock is BentoBoxV1 {\n constructor(IERC20 weth) public BentoBoxV1(weth) {\n return;\n }\n\n function addProfit(IERC20 token, uint256 amount) public {\n token.safeTransferFrom(msg.sender, address(this), amount);\n totals[token].addElastic(amount);\n }\n\n function takeLoss(IERC20 token, uint256 amount) public {\n token.safeTransfer(msg.sender, amount);\n totals[token].subElastic(amount);\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": [ + "ast" + ] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} \ No newline at end of file From 1f7d3a9c85d4f1b86b3ed553827446ac69a277b6 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 07:02:20 +0200 Subject: [PATCH 067/107] Add freely mintable ERC20 and use in Ropsten NFTPair mocks --- contracts/mocks/FreelyMintableERC20Mock.sol | 22 +++++++++++++++++++++ deploy/MockNFTPairEcoSystem.ts | 11 +++++++++++ 2 files changed, 33 insertions(+) create mode 100644 contracts/mocks/FreelyMintableERC20Mock.sol diff --git a/contracts/mocks/FreelyMintableERC20Mock.sol b/contracts/mocks/FreelyMintableERC20Mock.sol new file mode 100644 index 00000000..c1bdbec2 --- /dev/null +++ b/contracts/mocks/FreelyMintableERC20Mock.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "./ERC20Mock.sol"; + +contract FreelyMintableERC20Mock is ERC20Mock { + using BoringMath for uint256; + + constructor(uint256 initialSupply) public ERC20Mock(initialSupply) {} + + function mint(address to, uint256 amount) public { + totalSupply = totalSupply.add(amount); + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function burn(uint256 amount) public { + require(amount <= balanceOf[msg.sender], "MIM: not enough"); + totalSupply -= amount; + emit Transfer(msg.sender, address(0), amount); + } +} diff --git a/deploy/MockNFTPairEcoSystem.ts b/deploy/MockNFTPairEcoSystem.ts index c545b87a..c37c841e 100644 --- a/deploy/MockNFTPairEcoSystem.ts +++ b/deploy/MockNFTPairEcoSystem.ts @@ -59,6 +59,14 @@ const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnviro deterministicDeployment: false, }); + const freeMoneyMock = await deploy("FreeMoneyMock", { + contract: "FreelyMintableERC20Mock", + from: deployer, + args: [MaxUint128], + log: true, + deterministicDeployment: false, + }); + // Pairs - deployed by BentoBox: const bentoDeploy = async (name, masterAddress, initData) => { try { @@ -83,6 +91,9 @@ const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnviro await deployPair("ApesGuineasNFTPairMock", apesMock, guineasMock); await deployPair("ApesWethNFTPairMock", apesMock, wethMock); await deployPair("BearsGuineasNFTPairMock", bearsMock, guineasMock); + + await deployPair("ApesFreeMoneyNFTPairMock", apesMock, freeMoneyMock); + await deployPair("BearsFreeMoneyNFTPairMock", bearsMock, freeMoneyMock); }; export default deployFunction; From d808861f49406b56849554fbe01ca81f93b1072a Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 07:28:23 +0200 Subject: [PATCH 068/107] Deploy "Free Money" mock; redeploy NFT pair master (Ropsten) --- .../ropsten/ApesFreeMoneyNFTPairMock.json | 4 + .../ropsten/BearsFreeMoneyNFTPairMock.json | 4 + deployments/ropsten/FreeMoneyMock.json | 459 ++++++++++++++++++ deployments/ropsten/NFTPairMock.json | 86 ++-- .../495ac39dee8145a05ebd7dff96081707.json | 371 ++++++++++++++ 5 files changed, 881 insertions(+), 43 deletions(-) create mode 100644 deployments/ropsten/ApesFreeMoneyNFTPairMock.json create mode 100644 deployments/ropsten/BearsFreeMoneyNFTPairMock.json create mode 100644 deployments/ropsten/FreeMoneyMock.json create mode 100644 deployments/ropsten/solcInputs/495ac39dee8145a05ebd7dff96081707.json diff --git a/deployments/ropsten/ApesFreeMoneyNFTPairMock.json b/deployments/ropsten/ApesFreeMoneyNFTPairMock.json new file mode 100644 index 00000000..224e8013 --- /dev/null +++ b/deployments/ropsten/ApesFreeMoneyNFTPairMock.json @@ -0,0 +1,4 @@ +{ + "address": "0x9AEEf9f52eCCef2dc970090c304635fb29161805", + "abi": [] +} \ No newline at end of file diff --git a/deployments/ropsten/BearsFreeMoneyNFTPairMock.json b/deployments/ropsten/BearsFreeMoneyNFTPairMock.json new file mode 100644 index 00000000..1d569eab --- /dev/null +++ b/deployments/ropsten/BearsFreeMoneyNFTPairMock.json @@ -0,0 +1,4 @@ +{ + "address": "0xb215b44c3439cA72170F379f12A78437eACE9a19", + "abi": [] +} \ No newline at end of file diff --git a/deployments/ropsten/FreeMoneyMock.json b/deployments/ropsten/FreeMoneyMock.json new file mode 100644 index 00000000..edf2a4b3 --- /dev/null +++ b/deployments/ropsten/FreeMoneyMock.json @@ -0,0 +1,459 @@ +{ + "address": "0xEd79A29cE9F7E285be23E8fC32f74E5289713B86", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "initialSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner_", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0x159d804b2259d90c60693071fbc365047c8794aaadc48d2deb3a43d47f99934f", + "receipt": { + "to": null, + "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", + "contractAddress": "0xEd79A29cE9F7E285be23E8fC32f74E5289713B86", + "transactionIndex": 11, + "gasUsed": "747762", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x1d2e465a4e073be376ead2918f61d1b1b4bc2ed5131a593128d0576ec5ab4c83", + "transactionHash": "0x159d804b2259d90c60693071fbc365047c8794aaadc48d2deb3a43d47f99934f", + "logs": [], + "blockNumber": 12212539, + "cumulativeGasUsed": "1008152", + "status": 1, + "byzantium": true + }, + "args": [ + "340282366920938463463374607431768211455" + ], + "solcInputHash": "495ac39dee8145a05ebd7dff96081707", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"initialSupply\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"burn\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"mint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner_\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"spender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"permit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"approve(address,uint256)\":{\"params\":{\"amount\":\"The maximum collective amount that `spender` can draw.\",\"spender\":\"Address of the party that can draw from msg.sender's account.\"},\"returns\":{\"_0\":\"(bool) Returns True if approved.\"}},\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"deadline\":\"This permit must be redeemed before this deadline (UTC timestamp in seconds).\",\"owner_\":\"Address of the owner.\",\"spender\":\"The address of the spender that gets approved to draw from `owner_`.\",\"value\":\"The maximum collective amount that `spender` can draw.\"}},\"transfer(address,uint256)\":{\"params\":{\"amount\":\"of the tokens to move.\",\"to\":\"The address to move the tokens.\"},\"returns\":{\"_0\":\"(bool) Returns True if succeeded.\"}},\"transferFrom(address,address,uint256)\":{\"params\":{\"amount\":\"The token amount to move.\",\"from\":\"Address to draw tokens from.\",\"to\":\"The address to move the tokens.\"},\"returns\":{\"_0\":\"(bool) Returns True if succeeded.\"}}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"allowance(address,address)\":{\"notice\":\"owner > spender > allowance mapping.\"},\"approve(address,uint256)\":{\"notice\":\"Approves `amount` from sender to be spend by `spender`.\"},\"balanceOf(address)\":{\"notice\":\"owner > balance mapping.\"},\"nonces(address)\":{\"notice\":\"owner > nonce mapping. Used in `permit`.\"},\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Approves `value` from `owner_` to be spend by `spender`.\"},\"transfer(address,uint256)\":{\"notice\":\"Transfers `amount` tokens from `msg.sender` to `to`.\"},\"transferFrom(address,address,uint256)\":{\"notice\":\"Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/mocks/FreelyMintableERC20Mock.sol\":\"FreelyMintableERC20Mock\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/ERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./interfaces/IERC20.sol\\\";\\nimport \\\"./Domain.sol\\\";\\n\\n// solhint-disable no-inline-assembly\\n// solhint-disable not-rely-on-time\\n\\n// Data part taken out for building of contracts that receive delegate calls\\ncontract ERC20Data {\\n /// @notice owner > balance mapping.\\n mapping(address => uint256) public balanceOf;\\n /// @notice owner > spender > allowance mapping.\\n mapping(address => mapping(address => uint256)) public allowance;\\n /// @notice owner > nonce mapping. Used in `permit`.\\n mapping(address => uint256) public nonces;\\n}\\n\\nabstract contract ERC20 is IERC20, Domain {\\n /// @notice owner > balance mapping.\\n mapping(address => uint256) public override balanceOf;\\n /// @notice owner > spender > allowance mapping.\\n mapping(address => mapping(address => uint256)) public override allowance;\\n /// @notice owner > nonce mapping. Used in `permit`.\\n mapping(address => uint256) public nonces;\\n\\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\\n\\n /// @notice Transfers `amount` tokens from `msg.sender` to `to`.\\n /// @param to The address to move the tokens.\\n /// @param amount of the tokens to move.\\n /// @return (bool) Returns True if succeeded.\\n function transfer(address to, uint256 amount) public returns (bool) {\\n // If `amount` is 0, or `msg.sender` is `to` nothing happens\\n if (amount != 0 || msg.sender == to) {\\n uint256 srcBalance = balanceOf[msg.sender];\\n require(srcBalance >= amount, \\\"ERC20: balance too low\\\");\\n if (msg.sender != to) {\\n require(to != address(0), \\\"ERC20: no zero address\\\"); // Moved down so low balance calls safe some gas\\n\\n balanceOf[msg.sender] = srcBalance - amount; // Underflow is checked\\n balanceOf[to] += amount;\\n }\\n }\\n emit Transfer(msg.sender, to, amount);\\n return true;\\n }\\n\\n /// @notice Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\\n /// @param from Address to draw tokens from.\\n /// @param to The address to move the tokens.\\n /// @param amount The token amount to move.\\n /// @return (bool) Returns True if succeeded.\\n function transferFrom(\\n address from,\\n address to,\\n uint256 amount\\n ) public returns (bool) {\\n // If `amount` is 0, or `from` is `to` nothing happens\\n if (amount != 0) {\\n uint256 srcBalance = balanceOf[from];\\n require(srcBalance >= amount, \\\"ERC20: balance too low\\\");\\n\\n if (from != to) {\\n uint256 spenderAllowance = allowance[from][msg.sender];\\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\\n if (spenderAllowance != type(uint256).max) {\\n require(spenderAllowance >= amount, \\\"ERC20: allowance too low\\\");\\n allowance[from][msg.sender] = spenderAllowance - amount; // Underflow is checked\\n }\\n require(to != address(0), \\\"ERC20: no zero address\\\"); // Moved down so other failed calls safe some gas\\n\\n balanceOf[from] = srcBalance - amount; // Underflow is checked\\n balanceOf[to] += amount;\\n }\\n }\\n emit Transfer(from, to, amount);\\n return true;\\n }\\n\\n /// @notice Approves `amount` from sender to be spend by `spender`.\\n /// @param spender Address of the party that can draw from msg.sender's account.\\n /// @param amount The maximum collective amount that `spender` can draw.\\n /// @return (bool) Returns True if approved.\\n function approve(address spender, uint256 amount) public override returns (bool) {\\n allowance[msg.sender][spender] = amount;\\n emit Approval(msg.sender, spender, amount);\\n return true;\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // keccak256(\\\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\\\");\\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\\n\\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\\n /// @param owner_ Address of the owner.\\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\\n /// @param value The maximum collective amount that `spender` can draw.\\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\\n function permit(\\n address owner_,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external override {\\n require(owner_ != address(0), \\\"ERC20: Owner cannot be 0\\\");\\n require(block.timestamp < deadline, \\\"ERC20: Expired\\\");\\n require(\\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\\n owner_,\\n \\\"ERC20: Invalid Signature\\\"\\n );\\n allowance[owner_][spender] = value;\\n emit Approval(owner_, spender, value);\\n }\\n}\\n\\ncontract ERC20WithSupply is IERC20, ERC20 {\\n uint256 public override totalSupply;\\n\\n function _mint(address user, uint256 amount) internal {\\n uint256 newTotalSupply = totalSupply + amount;\\n require(newTotalSupply >= totalSupply, \\\"Mint overflow\\\");\\n totalSupply = newTotalSupply;\\n balanceOf[user] += amount;\\n emit Transfer(address(0), user, amount);\\n }\\n\\n function _burn(address user, uint256 amount) internal {\\n require(balanceOf[user] >= amount, \\\"Burn too much\\\");\\n totalSupply -= amount;\\n balanceOf[user] -= amount;\\n emit Transfer(user, address(0), amount);\\n }\\n}\\n\",\"keccak256\":\"0xda643d3376730a9b125c3daadc31f7cd1f867a1ed5aa7722948da429832ea85c\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"contracts/mocks/ERC20Mock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"@boringcrypto/boring-solidity/contracts/ERC20.sol\\\";\\n\\ncontract ERC20Mock is ERC20 {\\n uint256 public override totalSupply;\\n\\n constructor(uint256 _initialAmount) public {\\n // Give the creator all initial tokens\\n balanceOf[msg.sender] = _initialAmount;\\n // Update total supply\\n totalSupply = _initialAmount;\\n }\\n}\\n\",\"keccak256\":\"0xb424cd1870af45710e9d0fdd60bbe04c08138d6727500f0ad30ab37d4dd7989f\",\"license\":\"MIT\"},\"contracts/mocks/FreelyMintableERC20Mock.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\\\";\\nimport \\\"./ERC20Mock.sol\\\";\\n\\ncontract FreelyMintableERC20Mock is ERC20Mock {\\n using BoringMath for uint256;\\n\\n constructor(uint256 initialSupply) public ERC20Mock(initialSupply) {}\\n\\n function mint(address to, uint256 amount) public {\\n totalSupply = totalSupply.add(amount);\\n balanceOf[to] += amount;\\n emit Transfer(address(0), to, amount);\\n }\\n\\n function burn(uint256 amount) public {\\n require(amount <= balanceOf[msg.sender], \\\"MIM: not enough\\\");\\n totalSupply -= amount;\\n emit Transfer(msg.sender, address(0), amount);\\n }\\n}\\n\",\"keccak256\":\"0x3e005d38ba04259970b61f61bff77e30e50101f4d32804a7a2a837793d5097df\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60c060405234801561001057600080fd5b50604051610c97380380610c978339818101604052602081101561003357600080fd5b50514660a0819052819061004681610065565b60805250336000908152602081905260409020819055600355506100be565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692186020808301919091528183019390935230606080830191909152825180830390910181526080909101909152805191012090565b60805160a051610bb66100e1600039806109b85250806109ed5250610bb66000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461017457806370a08231146101915780637ecebe00146101b7578063a9059cbb146101dd578063d505accf14610209578063dd62ed3e1461025a576100a9565b8063095ea7b3146100ae57806318160ddd146100ee57806323b872dd146101085780633644e5151461013e57806340c10f1914610146575b600080fd5b6100da600480360360408110156100c457600080fd5b506001600160a01b038135169060200135610288565b604080519115158252519081900360200190f35b6100f66102ef565b60408051918252519081900360200190f35b6100da6004803603606081101561011e57600080fd5b506001600160a01b038135811691602081013590911690604001356102f5565b6100f66104ee565b6101726004803603604081101561015c57600080fd5b506001600160a01b0381351690602001356104fd565b005b6101726004803603602081101561018a57600080fd5b5035610550565b6100f6600480360360208110156101a757600080fd5b50356001600160a01b03166105d8565b6100f6600480360360208110156101cd57600080fd5b50356001600160a01b03166105ea565b6100da600480360360408110156101f357600080fd5b506001600160a01b0381351690602001356105fc565b610172600480360360e081101561021f57600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c0013561073e565b6100f66004803603604081101561027057600080fd5b506001600160a01b0381358116916020013516610996565b3360008181526001602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60035481565b600081156104ab576001600160a01b03841660009081526020819052604090205482811015610364576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b836001600160a01b0316856001600160a01b0316146104a9576001600160a01b0385166000908152600160209081526040808320338452909152902054600019811461042857838110156103ff576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20616c6c6f77616e636520746f6f206c6f770000000000000000604482015290519081900360640190fd5b6001600160a01b0386166000908152600160209081526040808320338452909152902084820390555b6001600160a01b03851661047c576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b506001600160a01b0380861660009081526020819052604080822086850390559186168152208054840190555b505b826001600160a01b0316846001600160a01b0316600080516020610b61833981519152846040518082815260200191505060405180910390a35060019392505050565b60006104f86109b3565b905090565b60035461050a9082610a13565b6003556001600160a01b03821660008181526020818152604080832080548601905580518581529051600080516020610b61833981519152929181900390910190a35050565b336000908152602081905260409020548111156105a6576040805162461bcd60e51b815260206004820152600f60248201526e09a929a7440dcdee840cadcdeeaced608b1b604482015290519081900360640190fd5b6003805482900390556040805182815290516000913391600080516020610b618339815191529181900360200190a350565b60006020819052908152604090205481565b60026020526000908152604090205481565b6000811515806106145750336001600160a01b038416145b15610707573360009081526020819052604090205482811015610677576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b336001600160a01b03851614610705576001600160a01b0384166106db576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b3360009081526020819052604080822085840390556001600160a01b038616825290208054840190555b505b6040805183815290516001600160a01b038516913391600080516020610b618339815191529181900360200190a350600192915050565b6001600160a01b038716610799576040805162461bcd60e51b815260206004820152601860248201527f45524332303a204f776e65722063616e6e6f7420626520300000000000000000604482015290519081900360640190fd5b8342106107de576040805162461bcd60e51b815260206004820152600e60248201526d115490cc8c0e88115e1c1a5c995960921b604482015290519081900360640190fd5b6001600160a01b038088166000818152600260209081526040918290208054600181810190925583517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981850152808501869052958c166060870152608086018b905260a086015260c08086018a90528351808703909101815260e0909501909252835193019290922090919061087490610a6b565b85858560405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156108cb573d6000803e3d6000fd5b505050602060405103516001600160a01b031614610930576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20496e76616c6964205369676e61747572650000000000000000604482015290519081900360640190fd5b6001600160a01b038088166000818152600160209081526040808320948b1680845294825291829020899055815189815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050505050565b600160209081526000928352604080842090915290825290205481565b6000467f000000000000000000000000000000000000000000000000000000000000000081146109eb576109e681610b07565b610a0d565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b818101818110156102e9576040805162461bcd60e51b815260206004820152601860248201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604482015290519081900360640190fd5b600060405180604001604052806002815260200161190160f01b815250610a906109b3565b836040516020018084805190602001908083835b60208310610ac35780518252601f199092019160209182019101610aa4565b51815160209384036101000a600019018019909216911617905292019485525083810192909252506040805180840383018152928101905281519101209392505050565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218602080830191909152818301939093523060608083019190915282518083039091018152608090910190915280519101209056feddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa26469706673582212207930a039119cf59aa3663224af157018b65ee4d51bddc840f5e4ba0e74a3e6a064736f6c634300060c0033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461017457806370a08231146101915780637ecebe00146101b7578063a9059cbb146101dd578063d505accf14610209578063dd62ed3e1461025a576100a9565b8063095ea7b3146100ae57806318160ddd146100ee57806323b872dd146101085780633644e5151461013e57806340c10f1914610146575b600080fd5b6100da600480360360408110156100c457600080fd5b506001600160a01b038135169060200135610288565b604080519115158252519081900360200190f35b6100f66102ef565b60408051918252519081900360200190f35b6100da6004803603606081101561011e57600080fd5b506001600160a01b038135811691602081013590911690604001356102f5565b6100f66104ee565b6101726004803603604081101561015c57600080fd5b506001600160a01b0381351690602001356104fd565b005b6101726004803603602081101561018a57600080fd5b5035610550565b6100f6600480360360208110156101a757600080fd5b50356001600160a01b03166105d8565b6100f6600480360360208110156101cd57600080fd5b50356001600160a01b03166105ea565b6100da600480360360408110156101f357600080fd5b506001600160a01b0381351690602001356105fc565b610172600480360360e081101561021f57600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c0013561073e565b6100f66004803603604081101561027057600080fd5b506001600160a01b0381358116916020013516610996565b3360008181526001602090815260408083206001600160a01b038716808552908352818420869055815186815291519394909390927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925928290030190a35060015b92915050565b60035481565b600081156104ab576001600160a01b03841660009081526020819052604090205482811015610364576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b836001600160a01b0316856001600160a01b0316146104a9576001600160a01b0385166000908152600160209081526040808320338452909152902054600019811461042857838110156103ff576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20616c6c6f77616e636520746f6f206c6f770000000000000000604482015290519081900360640190fd5b6001600160a01b0386166000908152600160209081526040808320338452909152902084820390555b6001600160a01b03851661047c576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b506001600160a01b0380861660009081526020819052604080822086850390559186168152208054840190555b505b826001600160a01b0316846001600160a01b0316600080516020610b61833981519152846040518082815260200191505060405180910390a35060019392505050565b60006104f86109b3565b905090565b60035461050a9082610a13565b6003556001600160a01b03821660008181526020818152604080832080548601905580518581529051600080516020610b61833981519152929181900390910190a35050565b336000908152602081905260409020548111156105a6576040805162461bcd60e51b815260206004820152600f60248201526e09a929a7440dcdee840cadcdeeaced608b1b604482015290519081900360640190fd5b6003805482900390556040805182815290516000913391600080516020610b618339815191529181900360200190a350565b60006020819052908152604090205481565b60026020526000908152604090205481565b6000811515806106145750336001600160a01b038416145b15610707573360009081526020819052604090205482811015610677576040805162461bcd60e51b815260206004820152601660248201527545524332303a2062616c616e636520746f6f206c6f7760501b604482015290519081900360640190fd5b336001600160a01b03851614610705576001600160a01b0384166106db576040805162461bcd60e51b815260206004820152601660248201527545524332303a206e6f207a65726f206164647265737360501b604482015290519081900360640190fd5b3360009081526020819052604080822085840390556001600160a01b038616825290208054840190555b505b6040805183815290516001600160a01b038516913391600080516020610b618339815191529181900360200190a350600192915050565b6001600160a01b038716610799576040805162461bcd60e51b815260206004820152601860248201527f45524332303a204f776e65722063616e6e6f7420626520300000000000000000604482015290519081900360640190fd5b8342106107de576040805162461bcd60e51b815260206004820152600e60248201526d115490cc8c0e88115e1c1a5c995960921b604482015290519081900360640190fd5b6001600160a01b038088166000818152600260209081526040918290208054600181810190925583517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981850152808501869052958c166060870152608086018b905260a086015260c08086018a90528351808703909101815260e0909501909252835193019290922090919061087490610a6b565b85858560405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156108cb573d6000803e3d6000fd5b505050602060405103516001600160a01b031614610930576040805162461bcd60e51b815260206004820152601860248201527f45524332303a20496e76616c6964205369676e61747572650000000000000000604482015290519081900360640190fd5b6001600160a01b038088166000818152600160209081526040808320948b1680845294825291829020899055815189815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a350505050505050565b600160209081526000928352604080842090915290825290205481565b6000467f000000000000000000000000000000000000000000000000000000000000000081146109eb576109e681610b07565b610a0d565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b818101818110156102e9576040805162461bcd60e51b815260206004820152601860248201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604482015290519081900360640190fd5b600060405180604001604052806002815260200161190160f01b815250610a906109b3565b836040516020018084805190602001908083835b60208310610ac35780518252601f199092019160209182019101610aa4565b51815160209384036101000a600019018019909216911617905292019485525083810192909252506040805180840383018152928101905281519101209392505050565b604080517f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218602080830191909152818301939093523060608083019190915282518083039091018152608090910190915280519101209056feddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa26469706673582212207930a039119cf59aa3663224af157018b65ee4d51bddc840f5e4ba0e74a3e6a064736f6c634300060c0033", + "devdoc": { + "kind": "dev", + "methods": { + "approve(address,uint256)": { + "params": { + "amount": "The maximum collective amount that `spender` can draw.", + "spender": "Address of the party that can draw from msg.sender's account." + }, + "returns": { + "_0": "(bool) Returns True if approved." + } + }, + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)": { + "params": { + "deadline": "This permit must be redeemed before this deadline (UTC timestamp in seconds).", + "owner_": "Address of the owner.", + "spender": "The address of the spender that gets approved to draw from `owner_`.", + "value": "The maximum collective amount that `spender` can draw." + } + }, + "transfer(address,uint256)": { + "params": { + "amount": "of the tokens to move.", + "to": "The address to move the tokens." + }, + "returns": { + "_0": "(bool) Returns True if succeeded." + } + }, + "transferFrom(address,address,uint256)": { + "params": { + "amount": "The token amount to move.", + "from": "Address to draw tokens from.", + "to": "The address to move the tokens." + }, + "returns": { + "_0": "(bool) Returns True if succeeded." + } + } + }, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "allowance(address,address)": { + "notice": "owner > spender > allowance mapping." + }, + "approve(address,uint256)": { + "notice": "Approves `amount` from sender to be spend by `spender`." + }, + "balanceOf(address)": { + "notice": "owner > balance mapping." + }, + "nonces(address)": { + "notice": "owner > nonce mapping. Used in `permit`." + }, + "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)": { + "notice": "Approves `value` from `owner_` to be spend by `spender`." + }, + "transfer(address,uint256)": { + "notice": "Transfers `amount` tokens from `msg.sender` to `to`." + }, + "transferFrom(address,address,uint256)": { + "notice": "Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [ + { + "astId": 924, + "contract": "contracts/mocks/FreelyMintableERC20Mock.sol:FreelyMintableERC20Mock", + "label": "balanceOf", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 932, + "contract": "contracts/mocks/FreelyMintableERC20Mock.sol:FreelyMintableERC20Mock", + "label": "allowance", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))" + }, + { + "astId": 937, + "contract": "contracts/mocks/FreelyMintableERC20Mock.sol:FreelyMintableERC20Mock", + "label": "nonces", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "astId": 21293, + "contract": "contracts/mocks/FreelyMintableERC20Mock.sol:FreelyMintableERC20Mock", + "label": "totalSupply", + "offset": 0, + "slot": "3", + "type": "t_uint256" + } + ], + "types": { + "t_address": { + "encoding": "inplace", + "label": "address", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32", + "value": "t_mapping(t_address,t_uint256)" + }, + "t_mapping(t_address,t_uint256)": { + "encoding": "mapping", + "key": "t_address", + "label": "mapping(address => uint256)", + "numberOfBytes": "32", + "value": "t_uint256" + }, + "t_uint256": { + "encoding": "inplace", + "label": "uint256", + "numberOfBytes": "32" + } + } + } +} \ No newline at end of file diff --git a/deployments/ropsten/NFTPairMock.json b/deployments/ropsten/NFTPairMock.json index 4708e587..60ca0974 100644 --- a/deployments/ropsten/NFTPairMock.json +++ b/deployments/ropsten/NFTPairMock.json @@ -1,5 +1,5 @@ { - "address": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", + "address": "0x3a341f5474aac54829a587cE6ab13C86af6B1E29", "abi": [ { "inputs": [ @@ -816,44 +816,44 @@ "type": "function" } ], - "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", + "transactionHash": "0x45ab4a0577a58d44883f9b5ecd9d948fb04a60456076d41f2d90b57b96fe1379", "receipt": { "to": null, "from": "0x63a1e3877b1662A9ad124f8611b06e3ffBC29Cba", - "contractAddress": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", - "transactionIndex": 13, + "contractAddress": "0x3a341f5474aac54829a587cE6ab13C86af6B1E29", + "transactionIndex": 18, "gasUsed": "3505038", - "logsBloom": "0x00000000000000000000000000000000000000002000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000020000000000000000000800000000000002000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000001000000000000000000000004000000000000010000000", - "blockHash": "0xe18e183a1e987327f0e2b01c997bad9e64d3e07f1bf7640966f24d6ab9836392", - "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", + "logsBloom": "0x00000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000100000000000000000000000000020000000000000000000800000000000002000000010000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000001000000000010000000", + "blockHash": "0xf9eb39bd0ab72664d7158749c8a75cdeb176abcf63e4ee75054e9218e2496741", + "transactionHash": "0x45ab4a0577a58d44883f9b5ecd9d948fb04a60456076d41f2d90b57b96fe1379", "logs": [ { - "transactionIndex": 13, - "blockNumber": 12212438, - "transactionHash": "0x015b0d7b534a0ae048b7e14b8dcaabe08580fefe0ea42814a9ca21feee40d788", - "address": "0x8E218fE3d4d00b4fb6Fd3ed743D71ee625496C8D", + "transactionIndex": 18, + "blockNumber": 12212537, + "transactionHash": "0x45ab4a0577a58d44883f9b5ecd9d948fb04a60456076d41f2d90b57b96fe1379", + "address": "0x3a341f5474aac54829a587cE6ab13C86af6B1E29", "topics": [ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000063a1e3877b1662a9ad124f8611b06e3ffbc29cba" ], "data": "0x", - "logIndex": 7, - "blockHash": "0xe18e183a1e987327f0e2b01c997bad9e64d3e07f1bf7640966f24d6ab9836392" + "logIndex": 5, + "blockHash": "0xf9eb39bd0ab72664d7158749c8a75cdeb176abcf63e4ee75054e9218e2496741" } ], - "blockNumber": 12212438, - "cumulativeGasUsed": "4102587", + "blockNumber": 12212537, + "cumulativeGasUsed": "4858265", "status": 1, "byzantium": true }, "args": [ "0x9A5620779feF1928eF87c1111491212efC2C3cB8" ], - "solcInputHash": "3285d4ce4a1fc523877d40dd9fd97bbb", - "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"bentoBox_\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"LogFeeTo\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogLend\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"LogRemoveCollateral\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogRepay\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogRequestLoan\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogUpdateLoanParams\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"feeTo\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"LogWithdrawFees\",\"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\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"asset\",\"outputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bentoBox\",\"outputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"principal\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"t\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"aprBPS\",\"type\":\"uint16\"}],\"name\":\"calculateInterest\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"interest\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"claimOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"collateral\",\"outputs\":[{\"internalType\":\"contract IERC721\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8[]\",\"name\":\"actions\",\"type\":\"uint8[]\"},{\"internalType\":\"uint256[]\",\"name\":\"values\",\"type\":\"uint256[]\"},{\"internalType\":\"bytes[]\",\"name\":\"datas\",\"type\":\"bytes[]\"}],\"name\":\"cook\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"value1\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"value2\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeTo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feesEarnedShare\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"init\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"accepted\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"lend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"masterContract\",\"outputs\":[{\"internalType\":\"contract NFTPair\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"removeCollateral\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"repay\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimCollateral\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"anyTokenId\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"requestAndBorrow\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"requestLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"setFeeTo\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimFunds\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"takeCollateralAndLend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoan\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"startTime\",\"type\":\"uint64\"},{\"internalType\":\"uint8\",\"name\":\"status\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoanParams\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"direct\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"renounce\",\"type\":\"bool\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"name\":\"updateLoanParams\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.\",\"kind\":\"dev\",\"methods\":{\"cook(uint8[],uint256[],bytes[])\":{\"params\":{\"actions\":\"An array with a sequence of actions to execute (see ACTION_ declarations).\",\"datas\":\"A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\",\"values\":\"A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\"},\"returns\":{\"value1\":\"May contain the first positioned return value of the last executed action (if applicable).\",\"value2\":\"May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\"}},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"params\":{\"accepted\":\"Loan parameters as the lender saw them, for security\",\"skim\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"removeCollateral(uint256,address)\":{\"params\":{\"to\":\"The receiver of the token.\",\"tokenId\":\"The token\"}},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"anyTokenId\":\"Set if lender agreed to any token. Must have tokenId 0 in signature.\",\"lender\":\"Lender, whose BentoBox balance the funds will come from\",\"params\":\"Loan parameters requested, and signed by the lender\",\"recipient\":\"Address to receive the loan.\",\"skimCollateral\":\"True if the collateral has already been transfered\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"params\":{\"params\":\"Loan conditions on offer\",\"skim\":\"True if the token has already been transfered\",\"to\":\"Address to receive the loan, or option to withdraw collateral\",\"tokenId\":\"ID of the NFT\"}},\"setFeeTo(address)\":{\"params\":{\"newFeeTo\":\"The address of the receiver.\"}},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"borrower\":\"Address that provides collateral and receives the loan\",\"params\":\"Loan terms offered, and signed by the borrower\",\"skimFunds\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"transferOwnership(address,bool,bool)\":{\"params\":{\"direct\":\"True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\",\"newOwner\":\"Address of the new owner.\",\"renounce\":\"Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\"}}},\"title\":\"NFTPair\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"calculateInterest(uint256,uint64,uint16)\":{\"notice\":\"Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected.\"},\"claimOwnership()\":{\"notice\":\"Needs to be called by `pendingOwner` to claim ownership.\"},\"constructor\":\"The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.\",\"cook(uint8[],uint256[],bytes[])\":{\"notice\":\"Executes a set of actions and allows composability (contract calls) to other contracts.\"},\"init(bytes)\":{\"notice\":\"De facto constructor for clone contracts\"},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"notice\":\"Lends with the parameters specified by the borrower.\"},\"removeCollateral(uint256,address)\":{\"notice\":\"Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan.\"},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Caller provides collateral; loan can go to a different address.\"},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"notice\":\"Deposit an NFT as collateral and request a loan against it\"},\"setFeeTo(address)\":{\"notice\":\"Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function.\"},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party.\"},\"transferOwnership(address,bool,bool)\":{\"notice\":\"Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`.\"},\"withdrawFees()\":{\"notice\":\"Withdraws the fees accumulated.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/NFTPair.sol\":\"NFTPair\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\\n// Edited by BoringCrypto\\n\\ncontract BoringOwnableData {\\n address public owner;\\n address public pendingOwner;\\n}\\n\\ncontract BoringOwnable is BoringOwnableData {\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /// @notice `owner` defaults to msg.sender on construction.\\n constructor() public {\\n owner = msg.sender;\\n emit OwnershipTransferred(address(0), msg.sender);\\n }\\n\\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\\n /// Can only be invoked by the current `owner`.\\n /// @param newOwner Address of the new owner.\\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\\n function transferOwnership(\\n address newOwner,\\n bool direct,\\n bool renounce\\n ) public onlyOwner {\\n if (direct) {\\n // Checks\\n require(newOwner != address(0) || renounce, \\\"Ownable: zero address\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, newOwner);\\n owner = newOwner;\\n pendingOwner = address(0);\\n } else {\\n // Effects\\n pendingOwner = newOwner;\\n }\\n }\\n\\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\\n function claimOwnership() public {\\n address _pendingOwner = pendingOwner;\\n\\n // Checks\\n require(msg.sender == _pendingOwner, \\\"Ownable: caller != pending owner\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, _pendingOwner);\\n owner = _pendingOwner;\\n pendingOwner = address(0);\\n }\\n\\n /// @notice Only allows the `owner` to execute the function.\\n modifier onlyOwner() {\\n require(msg.sender == owner, \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n}\\n\",\"keccak256\":\"0xbde1619421fef865bf5f5f806e319900fb862e27f0aef6e0878e93f04f477601\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IMasterContract {\\n /// @notice Init function that gets called from `BoringFactory.deploy`.\\n /// Also kown as the constructor for cloned contracts.\\n /// Any ETH send to `BoringFactory.deploy` ends up here.\\n /// @param data Can be abi encoded arguments or anything else.\\n function init(bytes calldata data) external payable;\\n}\\n\",\"keccak256\":\"0xc8d7519d2bd26fc6d5125f8fc3fe2a6aada76f71f26b4712e0a4160f1cbdb2ba\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"../interfaces/IERC20.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nlibrary BoringERC20 {\\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\\n\\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\\n if (data.length >= 64) {\\n return abi.decode(data, (string));\\n } else if (data.length == 32) {\\n uint8 i = 0;\\n while (i < 32 && data[i] != 0) {\\n i++;\\n }\\n bytes memory bytesArray = new bytes(i);\\n for (i = 0; i < 32 && data[i] != 0; i++) {\\n bytesArray[i] = data[i];\\n }\\n return string(bytesArray);\\n } else {\\n return \\\"???\\\";\\n }\\n }\\n\\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token symbol.\\n function safeSymbol(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token name.\\n function safeName(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (uint8) Token decimals.\\n function safeDecimals(IERC20 token) internal view returns (uint8) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\\n }\\n\\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\\n /// @param token The address of the ERC-20 token.\\n /// @param to The address of the user to check.\\n /// @return amount The token amount.\\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\\n require(success && data.length >= 32, \\\"BoringERC20: BalanceOf failed\\\");\\n amount = abi.decode(data, (uint256));\\n }\\n\\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransfer(\\n IERC20 token,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: Transfer failed\\\");\\n }\\n\\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param from Transfer tokens from.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransferFrom(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: TransferFrom failed\\\");\\n }\\n}\\n\",\"keccak256\":\"0xc0b0529bf740b422941fc4899762ef3bde7d05a56b1cdb60b063c2aa63883d65\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./BoringMath.sol\\\";\\n\\nstruct Rebase {\\n uint128 elastic;\\n uint128 base;\\n}\\n\\n/// @notice A rebasing library using overflow-/underflow-safe math.\\nlibrary RebaseLibrary {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n\\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\\n function toBase(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (uint256 base) {\\n if (total.elastic == 0) {\\n base = elastic;\\n } else {\\n base = elastic.mul(total.base) / total.elastic;\\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\\n base = base.add(1);\\n }\\n }\\n }\\n\\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\\n function toElastic(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (uint256 elastic) {\\n if (total.base == 0) {\\n elastic = base;\\n } else {\\n elastic = base.mul(total.elastic) / total.base;\\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\\n elastic = elastic.add(1);\\n }\\n }\\n }\\n\\n /// @notice Add `elastic` to `total` and doubles `total.base`.\\n /// @return (Rebase) The new total.\\n /// @return base in relationship to `elastic`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 base) {\\n base = toBase(total, elastic, roundUp);\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return (total, base);\\n }\\n\\n /// @notice Sub `base` from `total` and update `total.elastic`.\\n /// @return (Rebase) The new total.\\n /// @return elastic in relationship to `base`.\\n function sub(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 elastic) {\\n elastic = toElastic(total, base, roundUp);\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return (total, elastic);\\n }\\n\\n /// @notice Add `elastic` and `base` to `total`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return total;\\n }\\n\\n /// @notice Subtract `elastic` and `base` to `total`.\\n function sub(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return total;\\n }\\n\\n /// @notice Add `elastic` to `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.add(elastic.to128());\\n }\\n\\n /// @notice Subtract `elastic` from `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\\n }\\n}\\n\",\"keccak256\":\"0xab228bfa8a3019a4f7effa8aeeb05de141d328703d8a2f7b87ca811d0ca33196\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IBatchFlashBorrower {\\n function onBatchFlashLoan(\\n address sender,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n uint256[] calldata fees,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x825a46e61443df6e1289b513da4386d0413d0b5311553f3e7e7e5c90412ddd5d\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\\nimport './IBatchFlashBorrower.sol';\\nimport './IFlashBorrower.sol';\\nimport './IStrategy.sol';\\n\\ninterface IBentoBoxV1 {\\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\\n event LogRegisterProtocol(address indexed protocol);\\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\\n event LogStrategyDivest(address indexed token, uint256 amount);\\n event LogStrategyInvest(address indexed token, uint256 amount);\\n event LogStrategyLoss(address indexed token, uint256 amount);\\n event LogStrategyProfit(address indexed token, uint256 amount);\\n event LogStrategyQueued(address indexed token, address indexed strategy);\\n event LogStrategySet(address indexed token, address indexed strategy);\\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n function balanceOf(IERC20, address) external view returns (uint256);\\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\\n function claimOwnership() external;\\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\\n function masterContractApproved(address, address) external view returns (bool);\\n function masterContractOf(address) external view returns (address);\\n function nonces(address) external view returns (uint256);\\n function owner() external view returns (address);\\n function pendingOwner() external view returns (address);\\n function pendingStrategy(IERC20) external view returns (IStrategy);\\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\\n function registerProtocol() external;\\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\\n function strategy(IERC20) external view returns (IStrategy);\\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\\n function totals(IERC20) external view returns (Rebase memory totals_);\\n function transfer(IERC20 token, address from, address to, uint256 share) external;\\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\\n function whitelistMasterContract(address masterContract, bool approved) external;\\n function whitelistedMasterContracts(address) external view returns (bool);\\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\\n}\",\"keccak256\":\"0x9c025e34e0ef0c1fc9372ada9afa61925341ee93de9b9a79e77de55d715b6fb6\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IFlashBorrower {\\n function onFlashLoan(\\n address sender,\\n IERC20 token,\\n uint256 amount,\\n uint256 fee,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x6e389a5acb7b3e7f337b7e28477e998228f05fc4c8ff877eab32d3e15037ccc2\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IStrategy {\\n // Send the assets to the Strategy and call skim to invest them\\n function skim(uint256 amount) external;\\n\\n // Harvest any profits made converted to the asset and pass them to the caller\\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\\n\\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\\n\\n // Withdraw all assets in the safest way possible. This shouldn't fail.\\n function exit(uint256 balance) external returns (int256 amountAdded);\\n}\",\"keccak256\":\"0x91c02244e1508cf8e4d6c45110c57142301c237e809dcad67b8022f83555ba13\",\"license\":\"MIT\"},\"contracts/NFTPair.sol\":{\"content\":\"// SPDX-License-Identifier: UNLICENSED\\n\\n// Private Pool (NFT collateral)\\n\\n// ( ( (\\n// )\\\\ ) ( )\\\\ )\\\\ ) (\\n// (((_) ( /( ))\\\\ ((_)(()/( )( ( (\\n// )\\\\___ )(_)) /((_) _ ((_))(()\\\\ )\\\\ )\\\\ )\\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\\n// | (__ / _` || || || |/ _` | | '_|/ _ \\\\| ' \\\\))\\n// \\\\___|\\\\__,_| \\\\_,_||_|\\\\__,_| |_| \\\\___/|_||_|\\n\\n// Copyright (c) 2021 BoringCrypto - All rights reserved\\n// Twitter: @Boring_Crypto\\n\\n// Special thanks to:\\n// @0xKeno - for all his invaluable contributions\\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\\n\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/Domain.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\\\";\\nimport \\\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\\\";\\nimport \\\"./interfaces/IERC721.sol\\\";\\n\\nstruct TokenLoanParams {\\n uint128 valuation; // How much will you get? OK to owe until expiration.\\n uint64 duration; // Length of loan in seconds\\n uint16 annualInterestBPS; // Variable cost of taking out the loan\\n}\\n\\ninterface ILendingClub {\\n // Per token settings.\\n function willLend(uint256 tokenId, TokenLoanParams memory params)\\n external\\n view\\n returns (bool);\\n\\n function lendingConditions(address nftPair, uint256 tokenId)\\n external\\n view\\n returns (TokenLoanParams memory);\\n}\\n\\ninterface INFTPair {\\n function collateral() external view returns (IERC721);\\n\\n function asset() external view returns (IERC20);\\n\\n function masterContract() external view returns (address);\\n\\n function bentoBox() external view returns (IBentoBoxV1);\\n\\n function removeCollateral(uint256 tokenId, address to) external;\\n}\\n\\n/// @title NFTPair\\n/// @dev This contract allows contract calls to any contract (except BentoBox)\\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n using RebaseLibrary for Rebase;\\n using BoringERC20 for IERC20;\\n\\n event LogRequestLoan(\\n address indexed borrower,\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n event LogUpdateLoanParams(\\n uint256 indexed tokenId,\\n uint128 valuation,\\n uint64 duration,\\n uint16 annualInterestBPS\\n );\\n // This automatically clears the associated loan, if any\\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\\n // Details are in the loan request\\n event LogLend(address indexed lender, uint256 indexed tokenId);\\n event LogRepay(address indexed from, uint256 indexed tokenId);\\n event LogFeeTo(address indexed newFeeTo);\\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\\n\\n // Immutables (for MasterContract and all clones)\\n IBentoBoxV1 public immutable bentoBox;\\n NFTPair public immutable masterContract;\\n\\n // MasterContract variables\\n address public feeTo;\\n\\n // Per clone variables\\n // Clone init settings\\n IERC721 public collateral;\\n IERC20 public asset;\\n\\n // A note on terminology:\\n // \\\"Shares\\\" are BentoBox shares.\\n\\n // Track assets we own. Used to allow skimming the excesss.\\n uint256 public feesEarnedShare;\\n\\n // Per token settings.\\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\\n\\n uint8 private constant LOAN_INITIAL = 0;\\n uint8 private constant LOAN_REQUESTED = 1;\\n uint8 private constant LOAN_OUTSTANDING = 2;\\n struct TokenLoan {\\n address borrower;\\n address lender;\\n uint64 startTime;\\n uint8 status;\\n }\\n mapping(uint256 => TokenLoan) public tokenLoan;\\n\\n // Do not go over 100% on either of these..\\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\\n uint256 private constant OPEN_FEE_BPS = 100;\\n uint256 private constant BPS = 10_000;\\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\\n\\n // Highest order term in the Maclaurin series for exp used by\\n // `calculateIntest`.\\n // Intuitive interpretation: interest continuously accrues on the principal.\\n // That interest, in turn, earns \\\"second-order\\\" interest-on-interest, which\\n // itself earns \\\"third-order\\\" interest, etc. This constant determines how\\n // far we take this until we stop counting.\\n //\\n // The error, in terms of the interest rate, is at least\\n //\\n // ----- n ----- Infinity\\n // \\\\ x^k \\\\ x^k\\n // e^x - ) --- , which is ) --- ,\\n // / k! / k!\\n // ----- k = 1 k ----- k = n + 1\\n //\\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\\n // interest that is owed at rate r over time t. It makes no difference if\\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\\n // is the same. Why \\\"at least\\\"? There are also rounding errors. See\\n // `calculateInterest` for more detail.\\n // The factorial in the denominator \\\"wins\\\"; for all reasonable (and quite\\n // a few unreasonable) interest rates, the lower-order terms contribute the\\n // most to the total. The following table lists some of the calculated\\n // approximations for different values of n, along with the \\\"true\\\" result:\\n //\\n // Total: 10% 20% 50% 100% 200% 500% 1000%\\n // -----------------------------------------------------------------------\\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\\n //\\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\\n //\\n // For instance, calculating the compounding effects of 200% in \\\"total\\\"\\n // interest to the sixth order results in 635.6%, whereas the true result\\n // is 638.9%.\\n // At 500% that difference is a little more dramatic, but it is still in\\n // the same ballpark -- and of little practical consequence unless the\\n // collateral can be expected to go up more than 112 times in value.\\n // Still, for volatile tokens, or an asset that is somehow known to be very\\n // inflationary, use a different number.\\n // Zero (no interest at all) is ignored and treated as one (linear only).\\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\\n\\n // For signed lend / borrow requests:\\n mapping(address => uint256) public nonces;\\n\\n /// @notice The constructor is only used for the initial master contract.\\n /// @notice Subsequent clones are initialised via `init`.\\n constructor(IBentoBoxV1 bentoBox_) public {\\n bentoBox = bentoBox_;\\n masterContract = this;\\n }\\n\\n /// @notice De facto constructor for clone contracts\\n function init(bytes calldata data) public payable override {\\n require(\\n address(collateral) == address(0),\\n \\\"NFTPair: already initialized\\\"\\n );\\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\\n require(address(collateral) != address(0), \\\"NFTPair: bad pair\\\");\\n }\\n\\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params)\\n public\\n {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_OUTSTANDING) {\\n // The lender can change terms so long as the changes are strictly\\n // the same or better for the borrower:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\\n require(\\n params.duration >= cur.duration &&\\n params.valuation <= cur.valuation &&\\n params.annualInterestBPS <= cur.annualInterestBPS,\\n \\\"NFTPair: worse params\\\"\\n );\\n } else if (loan.status == LOAN_REQUESTED) {\\n // The borrower has already deposited the collateral and can\\n // change whatever they like\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else {\\n // The loan has not been taken out yet; the borrower needs to\\n // provide collateral.\\n revert(\\\"NFTPair: no collateral\\\");\\n }\\n tokenLoanParams[tokenId] = params;\\n emit LogUpdateLoanParams(\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n function _requestLoan(\\n address collateralProvider,\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) private {\\n // Edge case: valuation can be zero. That effectively gifts the NFT and\\n // is therefore a bad idea, but does not break the contract.\\n require(\\n tokenLoan[tokenId].status == LOAN_INITIAL,\\n \\\"NFTPair: loan exists\\\"\\n );\\n if (skim) {\\n require(\\n collateral.ownerOf(tokenId) == address(this),\\n \\\"NFTPair: skim failed\\\"\\n );\\n } else {\\n collateral.transferFrom(collateralProvider, address(this), tokenId);\\n }\\n TokenLoan memory loan;\\n loan.borrower = to;\\n loan.status = LOAN_REQUESTED;\\n tokenLoan[tokenId] = loan;\\n tokenLoanParams[tokenId] = params;\\n\\n emit LogRequestLoan(\\n to,\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS\\n );\\n }\\n\\n /// @notice Deposit an NFT as collateral and request a loan against it\\n /// @param tokenId ID of the NFT\\n /// @param to Address to receive the loan, or option to withdraw collateral\\n /// @param params Loan conditions on offer\\n /// @param skim True if the token has already been transfered\\n function requestLoan(\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) public {\\n _requestLoan(msg.sender, tokenId, params, to, skim);\\n }\\n\\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\\n /// @notice This destroys the loan.\\n /// @param tokenId The token\\n /// @param to The receiver of the token.\\n function removeCollateral(uint256 tokenId, address to) public {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_REQUESTED) {\\n // We are withdrawing collateral that is not in use:\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else if (loan.status == LOAN_OUTSTANDING) {\\n // We are seizing collateral as the lender. The loan has to be\\n // expired and not paid off:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <=\\n block.timestamp,\\n \\\"NFTPair: not expired\\\"\\n );\\n }\\n // If there somehow is collateral but no accompanying loan, then anyone\\n // can claim it by first requesting a loan with `skim` set to true, and\\n // then withdrawing. So we might as well allow it here..\\n delete tokenLoan[tokenId];\\n collateral.transferFrom(address(this), to, tokenId);\\n emit LogRemoveCollateral(tokenId, to);\\n }\\n\\n // Assumes the lender has agreed to the loan.\\n function _lend(\\n address lender,\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) internal {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_REQUESTED, \\\"NFTPair: not available\\\");\\n TokenLoanParams memory params = tokenLoanParams[tokenId];\\n\\n // Valuation has to be an exact match, everything else must be at least\\n // as good for the lender as `accepted`.\\n require(\\n params.valuation == accepted.valuation &&\\n params.duration <= accepted.duration &&\\n params.annualInterestBPS >= accepted.annualInterestBPS,\\n \\\"NFTPair: bad params\\\"\\n );\\n\\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\\n\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare -\\n openFeeShare +\\n protocolFeeShare +\\n feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n } else {\\n bentoBox.transfer(\\n asset,\\n lender,\\n address(this),\\n totalShare - openFeeShare + protocolFeeShare\\n );\\n }\\n // No underflow: follows from OPEN_FEE_BPS <= BPS\\n uint256 borrowerShare = totalShare - openFeeShare;\\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\\n // No overflow: addends (and result) must fit in BentoBox\\n feesEarnedShare += protocolFeeShare;\\n\\n loan.lender = lender;\\n loan.status = LOAN_OUTSTANDING;\\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\\n tokenLoan[tokenId] = loan;\\n\\n emit LogLend(lender, tokenId);\\n }\\n\\n /// @notice Lends with the parameters specified by the borrower.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param accepted Loan parameters as the lender saw them, for security\\n /// @param skim True if the funds have been transfered to the contract\\n function lend(\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) public {\\n _lend(msg.sender, tokenId, accepted, skim);\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // NOTE on signature hashes: the domain separator only guarantees that the\\n // chain ID and master contract are a match, so we explicitly include the\\n // clone address (and the asset/collateral addresses):\\n\\n // keccak256(\\\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant LEND_SIGNATURE_HASH =\\n 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\\n\\n // keccak256(\\\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant BORROW_SIGNATURE_HASH =\\n 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\\n\\n /// @notice Request and immediately borrow from a pre-committed lender\\n\\n /// @notice Caller provides collateral; loan can go to a different address.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param lender Lender, whose BentoBox balance the funds will come from\\n /// @param recipient Address to receive the loan.\\n /// @param params Loan parameters requested, and signed by the lender\\n /// @param skimCollateral True if the collateral has already been transfered\\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\\n function requestAndBorrow(\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\\n require(\\n ILendingClub(lender).willLend(tokenId, params),\\n \\\"NFTPair: LendingClub does not like you\\\"\\n );\\n } else {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[lender]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n LEND_SIGNATURE_HASH,\\n address(this),\\n anyTokenId ? 0 : tokenId,\\n anyTokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == lender,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n }\\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\\n _lend(lender, tokenId, params, false);\\n }\\n\\n /// @notice Take collateral from a pre-commited borrower and lend against it\\n /// @notice Collateral must come from the borrower, not a third party.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param borrower Address that provides collateral and receives the loan\\n /// @param params Loan terms offered, and signed by the borrower\\n /// @param skimFunds True if the funds have been transfered to the contract\\n function takeCollateralAndLend(\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[borrower]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n BORROW_SIGNATURE_HASH,\\n address(this),\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(\\n ecrecover(_getDigest(dataHash), v, r, s) == borrower,\\n \\\"NFTPair: signature invalid\\\"\\n );\\n _requestLoan(borrower, tokenId, params, borrower, false);\\n _lend(msg.sender, tokenId, params, skimFunds);\\n }\\n\\n /// Approximates continuous compounding. Uses Horner's method to evaluate\\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\\n /// errors along the way. The following is always guaranteed:\\n ///\\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\\n ///\\n /// where time = t/YEAR, up to at most the rounding error obtained in\\n /// calculating linear interest.\\n ///\\n /// If the theoretical result that we are approximating (the rightmost part\\n /// of the above inquality) fits in 128 bits, then the function is\\n /// guaranteed not to revert (unless n > 250, which is way too high).\\n /// If even the linear interest (leftmost part of the inequality) does not\\n /// the function will revert.\\n /// Otherwise, the function may revert, return a reasonable result, or\\n /// return a very inaccurate result. Even then the above inequality is\\n /// respected.\\n function calculateInterest(\\n uint256 principal,\\n uint64 t,\\n uint16 aprBPS\\n ) public pure returns (uint256 interest) {\\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\\n //\\n // We calculate\\n //\\n // ----- n ----- n\\n // \\\\ principal * (t * aprBPS)^k \\\\\\n // ) -------------------------- =: ) term_k\\n // / k! * YEAR_BPS^k /\\n // ----- k = 1 ----- k = 1\\n //\\n // which approaches, but never exceeds the \\\"theoretical\\\" result,\\n //\\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\\n //\\n // as n goes to infinity. We use the fact that\\n //\\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\\n // term_k = ---------------------------------------------\\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\\n //\\n // t * aprBPS\\n // = term_{k-1} * ------------ (*)\\n // k * YEAR_BPS\\n //\\n // to calculate the terms one by one. The principal affords us the\\n // precision to carry out the division without resorting to fixed-point\\n // math. Any rounding error is downward, which we consider acceptable.\\n //\\n // Since all numbers involved are positive, each term is certainly\\n // bounded above by M. From (*) we see that any intermediate results\\n // are at most\\n //\\n // denom_k := k * YEAR_BPS.\\n //\\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\\n // which proves that all calculations will certainly not overflow if M\\n // fits in 128 bits.\\n //\\n // If M does not fit, then the intermediate results for some term may\\n // eventually overflow, but this cannot happen at the first term, and\\n // neither can the total overflow because it uses checked math.\\n //\\n // This constitutes a guarantee of specified behavior when M >= 2^128.\\n uint256 x = uint256(t) * aprBPS;\\n uint256 term_k = (principal * x) / YEAR_BPS;\\n uint256 denom_k = YEAR_BPS;\\n\\n interest = term_k;\\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\\n denom_k += YEAR_BPS;\\n term_k = (term_k * x) / denom_k;\\n interest = interest.add(term_k); // <- Only overflow check we need\\n }\\n\\n if (interest >= 2**128) {\\n revert();\\n }\\n }\\n\\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_OUTSTANDING, \\\"NFTPair: no loan\\\");\\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\\n \\\"NFTPair: loan expired\\\"\\n );\\n\\n uint128 principal = loanParams.valuation;\\n\\n // No underflow: loan.startTime is only ever set to a block timestamp\\n // Cast is safe: if this overflows, then all loans have expired anyway\\n uint256 interest = calculateInterest(\\n principal,\\n uint64(block.timestamp - loan.startTime),\\n loanParams.annualInterestBPS\\n ).to128();\\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\\n amount = principal + interest;\\n\\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\\n\\n address from;\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >=\\n (totalShare + feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n from = address(this);\\n // No overflow: result fits in BentoBox\\n } else {\\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\\n from = msg.sender;\\n }\\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\\n feesEarnedShare += feeShare;\\n delete tokenLoan[tokenId];\\n\\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\\n collateral.transferFrom(address(this), loan.borrower, tokenId);\\n\\n emit LogRepay(from, tokenId);\\n }\\n\\n uint8 internal constant ACTION_REPAY = 2;\\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\\n\\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\\n uint8 internal constant ACTION_LEND = 13;\\n\\n // Function on BentoBox\\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\\n\\n // Any external call (except to BentoBox)\\n uint8 internal constant ACTION_CALL = 30;\\n\\n // Signed requests\\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\\n\\n int256 internal constant USE_VALUE1 = -1;\\n int256 internal constant USE_VALUE2 = -2;\\n\\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\\n function _num(\\n int256 inNum,\\n uint256 value1,\\n uint256 value2\\n ) internal pure returns (uint256 outNum) {\\n outNum = inNum >= 0\\n ? uint256(inNum)\\n : (inNum == USE_VALUE1 ? value1 : value2);\\n }\\n\\n /// @dev Helper function for depositing into `bentoBox`.\\n function _bentoDeposit(\\n bytes memory data,\\n uint256 value,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\\n share = int256(_num(share, value1, value2));\\n return\\n bentoBox.deposit{value: value}(\\n token,\\n msg.sender,\\n to,\\n uint256(amount),\\n uint256(share)\\n );\\n }\\n\\n /// @dev Helper function to withdraw from the `bentoBox`.\\n function _bentoWithdraw(\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(\\n data,\\n (IERC20, address, int256, int256)\\n );\\n return\\n bentoBox.withdraw(\\n token,\\n msg.sender,\\n to,\\n _num(amount, value1, value2),\\n _num(share, value1, value2)\\n );\\n }\\n\\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\\n /// This also means that calls made from this contract shall *not* be trusted.\\n function _call(\\n uint256 value,\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (bytes memory, uint8) {\\n (\\n address callee,\\n bytes memory callData,\\n bool useValue1,\\n bool useValue2,\\n uint8 returnValues\\n ) = abi.decode(data, (address, bytes, bool, bool, uint8));\\n\\n if (useValue1 && !useValue2) {\\n callData = abi.encodePacked(callData, value1);\\n } else if (!useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value2);\\n } else if (useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value1, value2);\\n }\\n\\n require(\\n callee != address(bentoBox) &&\\n callee != address(collateral) &&\\n callee != address(this),\\n \\\"NFTPair: can't call\\\"\\n );\\n\\n (bool success, bytes memory returnData) = callee.call{value: value}(\\n callData\\n );\\n require(success, \\\"NFTPair: call failed\\\");\\n return (returnData, returnValues);\\n }\\n\\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\\n function cook(\\n uint8[] calldata actions,\\n uint256[] calldata values,\\n bytes[] calldata datas\\n ) external payable returns (uint256 value1, uint256 value2) {\\n for (uint256 i = 0; i < actions.length; i++) {\\n uint8 action = actions[i];\\n if (action == ACTION_REPAY) {\\n (uint256 tokenId, bool skim) = abi.decode(\\n datas[i],\\n (uint256, bool)\\n );\\n repay(tokenId, skim);\\n } else if (action == ACTION_REMOVE_COLLATERAL) {\\n (uint256 tokenId, address to) = abi.decode(\\n datas[i],\\n (uint256, address)\\n );\\n removeCollateral(tokenId, to);\\n } else if (action == ACTION_REQUEST_LOAN) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) = abi.decode(\\n datas[i],\\n (uint256, TokenLoanParams, address, bool)\\n );\\n requestLoan(tokenId, params, to, skim);\\n } else if (action == ACTION_LEND) {\\n (\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n bool skim\\n ) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\\n lend(tokenId, params, skim);\\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\\n (\\n address user,\\n address _masterContract,\\n bool approved,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (address, address, bool, uint8, bytes32, bytes32)\\n );\\n bentoBox.setMasterContractApproval(\\n user,\\n _masterContract,\\n approved,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_BENTO_DEPOSIT) {\\n (value1, value2) = _bentoDeposit(\\n datas[i],\\n values[i],\\n value1,\\n value2\\n );\\n } else if (action == ACTION_BENTO_WITHDRAW) {\\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\\n } else if (action == ACTION_BENTO_TRANSFER) {\\n (IERC20 token, address to, int256 share) = abi.decode(\\n datas[i],\\n (IERC20, address, int256)\\n );\\n bentoBox.transfer(\\n token,\\n msg.sender,\\n to,\\n _num(share, value1, value2)\\n );\\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\\n (\\n IERC20 token,\\n address[] memory tos,\\n uint256[] memory shares\\n ) = abi.decode(datas[i], (IERC20, address[], uint256[]));\\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\\n } else if (action == ACTION_CALL) {\\n (bytes memory returnData, uint8 returnValues) = _call(\\n values[i],\\n datas[i],\\n value1,\\n value2\\n );\\n\\n if (returnValues == 1) {\\n (value1) = abi.decode(returnData, (uint256));\\n } else if (returnValues == 2) {\\n (value1, value2) = abi.decode(\\n returnData,\\n (uint256, uint256)\\n );\\n }\\n } else if (action == ACTION_REQUEST_AND_BORROW) {\\n (\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n address,\\n TokenLoanParams,\\n bool,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n requestAndBorrow(\\n tokenId,\\n lender,\\n recipient,\\n params,\\n skimCollateral,\\n anyTokenId,\\n deadline,\\n v,\\n r,\\n s\\n );\\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\\n (\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(\\n datas[i],\\n (\\n uint256,\\n address,\\n TokenLoanParams,\\n bool,\\n uint256,\\n uint8,\\n bytes32,\\n bytes32\\n )\\n );\\n takeCollateralAndLend(\\n tokenId,\\n borrower,\\n params,\\n skimFunds,\\n deadline,\\n v,\\n r,\\n s\\n );\\n }\\n }\\n }\\n\\n /// @notice Withdraws the fees accumulated.\\n function withdrawFees() public {\\n address to = masterContract.feeTo();\\n\\n uint256 _share = feesEarnedShare;\\n if (_share > 0) {\\n bentoBox.transfer(asset, address(this), to, _share);\\n feesEarnedShare = 0;\\n }\\n\\n emit LogWithdrawFees(to, _share);\\n }\\n\\n /// @notice Sets the beneficiary of fees accrued in liquidations.\\n /// MasterContract Only Admin function.\\n /// @param newFeeTo The address of the receiver.\\n function setFeeTo(address newFeeTo) public onlyOwner {\\n feeTo = newFeeTo;\\n emit LogFeeTo(newFeeTo);\\n }\\n}\\n\",\"keccak256\":\"0x2f116b73330c8b296bd38f699afc82f62753a7ced6b6a85a3f4e0cc717c4351c\",\"license\":\"UNLICENSED\"},\"contracts/interfaces/IERC165.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.0 <0.8.0;\\n\\n/**\\n * @dev Interface of the ERC165 standard, as defined in the\\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\\n *\\n * Implementers can declare support of contract interfaces, which can then be\\n * queried by others ({ERC165Checker}).\\n *\\n * For an implementation, see {ERC165}.\\n */\\ninterface IERC165 {\\n /**\\n * @dev Returns true if this contract implements the interface defined by\\n * `interfaceId`. See the corresponding\\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\\n * to learn more about how these ids are created.\\n *\\n * This function call must use less than 30 000 gas.\\n */\\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\\n}\\n\",\"keccak256\":\"0x568574015c35b45a03f3bc3857240fb9985380d3faa3df7207123620d48ffe13\",\"license\":\"MIT\"},\"contracts/interfaces/IERC721.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.2 <0.8.0;\\n\\nimport \\\"./IERC165.sol\\\";\\n\\n/**\\n * @dev Required interface of an ERC721 compliant contract.\\n */\\ninterface IERC721 is IERC165 {\\n /**\\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\\n */\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\\n */\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\\n */\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n /**\\n * @dev Returns the number of tokens in ``owner``'s account.\\n */\\n function balanceOf(address owner) external view returns (uint256 balance);\\n\\n /**\\n * @dev Returns the owner of the `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function ownerOf(uint256 tokenId) external view returns (address owner);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Transfers `tokenId` token from `from` to `to`.\\n *\\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\\n * The approval is cleared when the token is transferred.\\n *\\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\\n *\\n * Requirements:\\n *\\n * - The caller must own the token or be an approved operator.\\n * - `tokenId` must exist.\\n *\\n * Emits an {Approval} event.\\n */\\n function approve(address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Returns the account approved for `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function getApproved(uint256 tokenId) external view returns (address operator);\\n\\n /**\\n * @dev Approve or remove `operator` as an operator for the caller.\\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\\n *\\n * Requirements:\\n *\\n * - The `operator` cannot be the caller.\\n *\\n * Emits an {ApprovalForAll} event.\\n */\\n function setApprovalForAll(address operator, bool _approved) external;\\n\\n /**\\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\\n *\\n * See {setApprovalForAll}\\n */\\n function isApprovedForAll(address owner, address operator) external view returns (bool);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\\n}\\n\",\"keccak256\":\"0x0bfe878a4a6ddcdba3d5b53a21e76bcb84bc77a114fbd432a5533bda12f155fa\",\"license\":\"MIT\"}},\"version\":1}", - "bytecode": "0x6101006040523480156200001257600080fd5b5060405162003fdf38038062003fdf8339810160408190526200003591620000fd565b600080546001600160a01b0319163390811782556040519091907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a34660a08190526200008581620000a7565b608052506001600160601b0319606091821b1660c05230901b60e0526200014c565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692188230604051602001620000e0939291906200012d565b604051602081830303815290604052805190602001209050919050565b6000602082840312156200010f578081fd5b81516001600160a01b038116811462000126578182fd5b9392505050565b92835260208301919091526001600160a01b0316604082015260600190565b60805160a05160c05160601c60e05160601c613e02620001dd600039806108ba5280611b825250806109705280610d115280610ed95280610fb152806111d7528061160352806116af5280611764528061182b52806118f35280611f115280611fed528061219c52806126fc52806127c5528061288a528061291d525080611e75525080611eaa5250613e026000f3fe6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", - "deployedBytecode": "0x6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea26469706673582212200c48e1e14a56eb5dd85d2e7d61edca5ef9ba7ceadd69322492cc8015f418b4d464736f6c634300060c0033", + "solcInputHash": "495ac39dee8145a05ebd7dff96081707", + "metadata": "{\"compiler\":{\"version\":\"0.6.12+commit.27d51765\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"bentoBox_\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"LogFeeTo\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogLend\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"LogRemoveCollateral\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"}],\"name\":\"LogRepay\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogRequestLoan\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"name\":\"LogUpdateLoanParams\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"feeTo\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"feeShare\",\"type\":\"uint256\"}],\"name\":\"LogWithdrawFees\",\"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\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"asset\",\"outputs\":[{\"internalType\":\"contract IERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bentoBox\",\"outputs\":[{\"internalType\":\"contract IBentoBoxV1\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"principal\",\"type\":\"uint256\"},{\"internalType\":\"uint64\",\"name\":\"t\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"aprBPS\",\"type\":\"uint16\"}],\"name\":\"calculateInterest\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"interest\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"claimOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"collateral\",\"outputs\":[{\"internalType\":\"contract IERC721\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8[]\",\"name\":\"actions\",\"type\":\"uint8[]\"},{\"internalType\":\"uint256[]\",\"name\":\"values\",\"type\":\"uint256[]\"},{\"internalType\":\"bytes[]\",\"name\":\"datas\",\"type\":\"bytes[]\"}],\"name\":\"cook\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"value1\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"value2\",\"type\":\"uint256\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feeTo\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"feesEarnedShare\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"init\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"accepted\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"lend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"masterContract\",\"outputs\":[{\"internalType\":\"contract NFTPair\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"name\":\"nonces\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"pendingOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"removeCollateral\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"repay\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimCollateral\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"anyTokenId\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"requestAndBorrow\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"skim\",\"type\":\"bool\"}],\"name\":\"requestLoan\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newFeeTo\",\"type\":\"address\"}],\"name\":\"setFeeTo\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"},{\"internalType\":\"bool\",\"name\":\"skimFunds\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"deadline\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"takeCollateralAndLend\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoan\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"borrower\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"lender\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"startTime\",\"type\":\"uint64\"},{\"internalType\":\"uint8\",\"name\":\"status\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"tokenLoanParams\",\"outputs\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"direct\",\"type\":\"bool\"},{\"internalType\":\"bool\",\"name\":\"renounce\",\"type\":\"bool\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"tokenId\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint128\",\"name\":\"valuation\",\"type\":\"uint128\"},{\"internalType\":\"uint64\",\"name\":\"duration\",\"type\":\"uint64\"},{\"internalType\":\"uint16\",\"name\":\"annualInterestBPS\",\"type\":\"uint16\"}],\"internalType\":\"struct TokenLoanParams\",\"name\":\"params\",\"type\":\"tuple\"}],\"name\":\"updateLoanParams\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawFees\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.\",\"kind\":\"dev\",\"methods\":{\"cook(uint8[],uint256[],bytes[])\":{\"params\":{\"actions\":\"An array with a sequence of actions to execute (see ACTION_ declarations).\",\"datas\":\"A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\",\"values\":\"A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\"},\"returns\":{\"value1\":\"May contain the first positioned return value of the last executed action (if applicable).\",\"value2\":\"May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\"}},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"params\":{\"accepted\":\"Loan parameters as the lender saw them, for security\",\"skim\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"removeCollateral(uint256,address)\":{\"params\":{\"to\":\"The receiver of the token.\",\"tokenId\":\"The token\"}},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"anyTokenId\":\"Set if lender agreed to any token. Must have tokenId 0 in signature.\",\"lender\":\"Lender, whose BentoBox balance the funds will come from\",\"params\":\"Loan parameters requested, and signed by the lender\",\"recipient\":\"Address to receive the loan.\",\"skimCollateral\":\"True if the collateral has already been transfered\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"params\":{\"params\":\"Loan conditions on offer\",\"skim\":\"True if the token has already been transfered\",\"to\":\"Address to receive the loan, or option to withdraw collateral\",\"tokenId\":\"ID of the NFT\"}},\"setFeeTo(address)\":{\"params\":{\"newFeeTo\":\"The address of the receiver.\"}},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"params\":{\"borrower\":\"Address that provides collateral and receives the loan\",\"params\":\"Loan terms offered, and signed by the borrower\",\"skimFunds\":\"True if the funds have been transfered to the contract\",\"tokenId\":\"ID of the token that will function as collateral\"}},\"transferOwnership(address,bool,bool)\":{\"params\":{\"direct\":\"True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\",\"newOwner\":\"Address of the new owner.\",\"renounce\":\"Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\"}}},\"title\":\"NFTPair\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"calculateInterest(uint256,uint64,uint16)\":{\"notice\":\"Approximates continuous compounding. Uses Horner's method to evaluate the truncated Maclaurin series for exp - 1, accumulating rounding errors along the way. The following is always guaranteed: principal * time * apr <= result <= principal * (e^(time * apr) - 1), where time = t/YEAR, up to at most the rounding error obtained in calculating linear interest. If the theoretical result that we are approximating (the rightmost part of the above inquality) fits in 128 bits, then the function is guaranteed not to revert (unless n > 250, which is way too high). If even the linear interest (leftmost part of the inequality) does not the function will revert. Otherwise, the function may revert, return a reasonable result, or return a very inaccurate result. Even then the above inequality is respected.\"},\"claimOwnership()\":{\"notice\":\"Needs to be called by `pendingOwner` to claim ownership.\"},\"constructor\":\"The constructor is only used for the initial master contract.Subsequent clones are initialised via `init`.\",\"cook(uint8[],uint256[],bytes[])\":{\"notice\":\"Executes a set of actions and allows composability (contract calls) to other contracts.\"},\"init(bytes)\":{\"notice\":\"De facto constructor for clone contracts\"},\"lend(uint256,(uint128,uint64,uint16),bool)\":{\"notice\":\"Lends with the parameters specified by the borrower.\"},\"removeCollateral(uint256,address)\":{\"notice\":\"Removes `tokenId` as collateral and transfers it to `to`.This destroys the loan.\"},\"requestAndBorrow(uint256,address,address,(uint128,uint64,uint16),bool,bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Caller provides collateral; loan can go to a different address.\"},\"requestLoan(uint256,(uint128,uint64,uint16),address,bool)\":{\"notice\":\"Deposit an NFT as collateral and request a loan against it\"},\"setFeeTo(address)\":{\"notice\":\"Sets the beneficiary of fees accrued in liquidations. MasterContract Only Admin function.\"},\"takeCollateralAndLend(uint256,address,(uint128,uint64,uint16),bool,uint256,uint8,bytes32,bytes32)\":{\"notice\":\"Take collateral from a pre-commited borrower and lend against itCollateral must come from the borrower, not a third party.\"},\"transferOwnership(address,bool,bool)\":{\"notice\":\"Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner. Can only be invoked by the current `owner`.\"},\"withdrawFees()\":{\"notice\":\"Withdraws the fees accumulated.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/NFTPair.sol\":\"NFTPair\"},\"evmVersion\":\"istanbul\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[]},\"sources\":{\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\\n// Edited by BoringCrypto\\n\\ncontract BoringOwnableData {\\n address public owner;\\n address public pendingOwner;\\n}\\n\\ncontract BoringOwnable is BoringOwnableData {\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n\\n /// @notice `owner` defaults to msg.sender on construction.\\n constructor() public {\\n owner = msg.sender;\\n emit OwnershipTransferred(address(0), msg.sender);\\n }\\n\\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\\n /// Can only be invoked by the current `owner`.\\n /// @param newOwner Address of the new owner.\\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\\n function transferOwnership(\\n address newOwner,\\n bool direct,\\n bool renounce\\n ) public onlyOwner {\\n if (direct) {\\n // Checks\\n require(newOwner != address(0) || renounce, \\\"Ownable: zero address\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, newOwner);\\n owner = newOwner;\\n pendingOwner = address(0);\\n } else {\\n // Effects\\n pendingOwner = newOwner;\\n }\\n }\\n\\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\\n function claimOwnership() public {\\n address _pendingOwner = pendingOwner;\\n\\n // Checks\\n require(msg.sender == _pendingOwner, \\\"Ownable: caller != pending owner\\\");\\n\\n // Effects\\n emit OwnershipTransferred(owner, _pendingOwner);\\n owner = _pendingOwner;\\n pendingOwner = address(0);\\n }\\n\\n /// @notice Only allows the `owner` to execute the function.\\n modifier onlyOwner() {\\n require(msg.sender == owner, \\\"Ownable: caller is not the owner\\\");\\n _;\\n }\\n}\\n\",\"keccak256\":\"0xbde1619421fef865bf5f5f806e319900fb862e27f0aef6e0878e93f04f477601\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/Domain.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Based on code and smartness by Ross Campbell and Keno\\n// Uses immutable to store the domain separator to reduce gas usage\\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\\npragma solidity 0.6.12;\\n\\n// solhint-disable no-inline-assembly\\n\\ncontract Domain {\\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\\\"EIP712Domain(uint256 chainId,address verifyingContract)\\\");\\n // See https://eips.ethereum.org/EIPS/eip-191\\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \\\"\\\\x19\\\\x01\\\";\\n\\n // solhint-disable var-name-mixedcase\\n bytes32 private immutable _DOMAIN_SEPARATOR;\\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\\n\\n /// @dev Calculate the DOMAIN_SEPARATOR\\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\\n }\\n\\n constructor() public {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\\n }\\n\\n /// @dev Return the DOMAIN_SEPARATOR\\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\\n // solhint-disable-next-line func-name-mixedcase\\n function _domainSeparator() internal view returns (bytes32) {\\n uint256 chainId;\\n assembly {\\n chainId := chainid()\\n }\\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\\n }\\n\\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\\n }\\n}\\n\",\"keccak256\":\"0xbcd071bfa82a5deb12c8e21ec4c2fb25f2f0b805009d9712221eb52f9d05f1c1\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IERC20 {\\n function totalSupply() external view returns (uint256);\\n\\n function balanceOf(address account) external view returns (uint256);\\n\\n function allowance(address owner, address spender) external view returns (uint256);\\n\\n function approve(address spender, uint256 amount) external returns (bool);\\n\\n event Transfer(address indexed from, address indexed to, uint256 value);\\n event Approval(address indexed owner, address indexed spender, uint256 value);\\n\\n /// @notice EIP 2612\\n function permit(\\n address owner,\\n address spender,\\n uint256 value,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) external;\\n}\\n\",\"keccak256\":\"0xf0da35541d6ae9e3c12fdd7c8d5d9584c56f9ac50d062efb8ca353ebd6ffd47d\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IMasterContract {\\n /// @notice Init function that gets called from `BoringFactory.deploy`.\\n /// Also kown as the constructor for cloned contracts.\\n /// Any ETH send to `BoringFactory.deploy` ends up here.\\n /// @param data Can be abi encoded arguments or anything else.\\n function init(bytes calldata data) external payable;\\n}\\n\",\"keccak256\":\"0xc8d7519d2bd26fc6d5125f8fc3fe2a6aada76f71f26b4712e0a4160f1cbdb2ba\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"../interfaces/IERC20.sol\\\";\\n\\n// solhint-disable avoid-low-level-calls\\n\\nlibrary BoringERC20 {\\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\\n\\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\\n if (data.length >= 64) {\\n return abi.decode(data, (string));\\n } else if (data.length == 32) {\\n uint8 i = 0;\\n while (i < 32 && data[i] != 0) {\\n i++;\\n }\\n bytes memory bytesArray = new bytes(i);\\n for (i = 0; i < 32 && data[i] != 0; i++) {\\n bytesArray[i] = data[i];\\n }\\n return string(bytesArray);\\n } else {\\n return \\\"???\\\";\\n }\\n }\\n\\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token symbol.\\n function safeSymbol(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (string) Token name.\\n function safeName(IERC20 token) internal view returns (string memory) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\\n return success ? returnDataToString(data) : \\\"???\\\";\\n }\\n\\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\\n /// @param token The address of the ERC-20 token contract.\\n /// @return (uint8) Token decimals.\\n function safeDecimals(IERC20 token) internal view returns (uint8) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\\n }\\n\\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\\n /// @param token The address of the ERC-20 token.\\n /// @param to The address of the user to check.\\n /// @return amount The token amount.\\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\\n require(success && data.length >= 32, \\\"BoringERC20: BalanceOf failed\\\");\\n amount = abi.decode(data, (uint256));\\n }\\n\\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransfer(\\n IERC20 token,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: Transfer failed\\\");\\n }\\n\\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\\n /// Reverts on a failed transfer.\\n /// @param token The address of the ERC-20 token.\\n /// @param from Transfer tokens from.\\n /// @param to Transfer tokens to.\\n /// @param amount The token amount.\\n function safeTransferFrom(\\n IERC20 token,\\n address from,\\n address to,\\n uint256 amount\\n ) internal {\\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\\n require(success && (data.length == 0 || abi.decode(data, (bool))), \\\"BoringERC20: TransferFrom failed\\\");\\n }\\n}\\n\",\"keccak256\":\"0xc0b0529bf740b422941fc4899762ef3bde7d05a56b1cdb60b063c2aa63883d65\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\n/// @notice A library for performing overflow-/underflow-safe math,\\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\\nlibrary BoringMath {\\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n\\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\\n require(b == 0 || (c = a * b) / b == a, \\\"BoringMath: Mul Overflow\\\");\\n }\\n\\n function to128(uint256 a) internal pure returns (uint128 c) {\\n require(a <= uint128(-1), \\\"BoringMath: uint128 Overflow\\\");\\n c = uint128(a);\\n }\\n\\n function to64(uint256 a) internal pure returns (uint64 c) {\\n require(a <= uint64(-1), \\\"BoringMath: uint64 Overflow\\\");\\n c = uint64(a);\\n }\\n\\n function to32(uint256 a) internal pure returns (uint32 c) {\\n require(a <= uint32(-1), \\\"BoringMath: uint32 Overflow\\\");\\n c = uint32(a);\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\\nlibrary BoringMath128 {\\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\\nlibrary BoringMath64 {\\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\\nlibrary BoringMath32 {\\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a + b) >= b, \\\"BoringMath: Add Overflow\\\");\\n }\\n\\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\\n require((c = a - b) <= a, \\\"BoringMath: Underflow\\\");\\n }\\n}\\n\",\"keccak256\":\"0x6bc52950e23c70a90a5b039697b77ba76360b62da6a06a61d3a1714b9c6c26b9\",\"license\":\"MIT\"},\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport \\\"./BoringMath.sol\\\";\\n\\nstruct Rebase {\\n uint128 elastic;\\n uint128 base;\\n}\\n\\n/// @notice A rebasing library using overflow-/underflow-safe math.\\nlibrary RebaseLibrary {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n\\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\\n function toBase(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (uint256 base) {\\n if (total.elastic == 0) {\\n base = elastic;\\n } else {\\n base = elastic.mul(total.base) / total.elastic;\\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\\n base = base.add(1);\\n }\\n }\\n }\\n\\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\\n function toElastic(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (uint256 elastic) {\\n if (total.base == 0) {\\n elastic = base;\\n } else {\\n elastic = base.mul(total.elastic) / total.base;\\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\\n elastic = elastic.add(1);\\n }\\n }\\n }\\n\\n /// @notice Add `elastic` to `total` and doubles `total.base`.\\n /// @return (Rebase) The new total.\\n /// @return base in relationship to `elastic`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 base) {\\n base = toBase(total, elastic, roundUp);\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return (total, base);\\n }\\n\\n /// @notice Sub `base` from `total` and update `total.elastic`.\\n /// @return (Rebase) The new total.\\n /// @return elastic in relationship to `base`.\\n function sub(\\n Rebase memory total,\\n uint256 base,\\n bool roundUp\\n ) internal pure returns (Rebase memory, uint256 elastic) {\\n elastic = toElastic(total, base, roundUp);\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return (total, elastic);\\n }\\n\\n /// @notice Add `elastic` and `base` to `total`.\\n function add(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.add(elastic.to128());\\n total.base = total.base.add(base.to128());\\n return total;\\n }\\n\\n /// @notice Subtract `elastic` and `base` to `total`.\\n function sub(\\n Rebase memory total,\\n uint256 elastic,\\n uint256 base\\n ) internal pure returns (Rebase memory) {\\n total.elastic = total.elastic.sub(elastic.to128());\\n total.base = total.base.sub(base.to128());\\n return total;\\n }\\n\\n /// @notice Add `elastic` to `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.add(elastic.to128());\\n }\\n\\n /// @notice Subtract `elastic` from `total` and update storage.\\n /// @return newElastic Returns updated `elastic`.\\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\\n }\\n}\\n\",\"keccak256\":\"0xab228bfa8a3019a4f7effa8aeeb05de141d328703d8a2f7b87ca811d0ca33196\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IBatchFlashBorrower {\\n function onBatchFlashLoan(\\n address sender,\\n IERC20[] calldata tokens,\\n uint256[] calldata amounts,\\n uint256[] calldata fees,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x825a46e61443df6e1289b513da4386d0413d0b5311553f3e7e7e5c90412ddd5d\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\n\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\\nimport './IBatchFlashBorrower.sol';\\nimport './IFlashBorrower.sol';\\nimport './IStrategy.sol';\\n\\ninterface IBentoBoxV1 {\\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\\n event LogRegisterProtocol(address indexed protocol);\\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\\n event LogStrategyDivest(address indexed token, uint256 amount);\\n event LogStrategyInvest(address indexed token, uint256 amount);\\n event LogStrategyLoss(address indexed token, uint256 amount);\\n event LogStrategyProfit(address indexed token, uint256 amount);\\n event LogStrategyQueued(address indexed token, address indexed strategy);\\n event LogStrategySet(address indexed token, address indexed strategy);\\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\\n function balanceOf(IERC20, address) external view returns (uint256);\\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\\n function claimOwnership() external;\\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\\n function masterContractApproved(address, address) external view returns (bool);\\n function masterContractOf(address) external view returns (address);\\n function nonces(address) external view returns (uint256);\\n function owner() external view returns (address);\\n function pendingOwner() external view returns (address);\\n function pendingStrategy(IERC20) external view returns (IStrategy);\\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\\n function registerProtocol() external;\\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\\n function strategy(IERC20) external view returns (IStrategy);\\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\\n function totals(IERC20) external view returns (Rebase memory totals_);\\n function transfer(IERC20 token, address from, address to, uint256 share) external;\\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\\n function whitelistMasterContract(address masterContract, bool approved) external;\\n function whitelistedMasterContracts(address) external view returns (bool);\\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\\n}\",\"keccak256\":\"0x9c025e34e0ef0c1fc9372ada9afa61925341ee93de9b9a79e77de55d715b6fb6\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\\n\\ninterface IFlashBorrower {\\n function onFlashLoan(\\n address sender,\\n IERC20 token,\\n uint256 amount,\\n uint256 fee,\\n bytes calldata data\\n ) external;\\n}\",\"keccak256\":\"0x6e389a5acb7b3e7f337b7e28477e998228f05fc4c8ff877eab32d3e15037ccc2\",\"license\":\"MIT\"},\"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity 0.6.12;\\n\\ninterface IStrategy {\\n // Send the assets to the Strategy and call skim to invest them\\n function skim(uint256 amount) external;\\n\\n // Harvest any profits made converted to the asset and pass them to the caller\\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\\n\\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\\n\\n // Withdraw all assets in the safest way possible. This shouldn't fail.\\n function exit(uint256 balance) external returns (int256 amountAdded);\\n}\",\"keccak256\":\"0x91c02244e1508cf8e4d6c45110c57142301c237e809dcad67b8022f83555ba13\",\"license\":\"MIT\"},\"contracts/NFTPair.sol\":{\"content\":\"// SPDX-License-Identifier: UNLICENSED\\n\\n// Private Pool (NFT collateral)\\n\\n// ( ( (\\n// )\\\\ ) ( )\\\\ )\\\\ ) (\\n// (((_) ( /( ))\\\\ ((_)(()/( )( ( (\\n// )\\\\___ )(_)) /((_) _ ((_))(()\\\\ )\\\\ )\\\\ )\\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\\n// | (__ / _` || || || |/ _` | | '_|/ _ \\\\| ' \\\\))\\n// \\\\___|\\\\__,_| \\\\_,_||_|\\\\__,_| |_| \\\\___/|_||_|\\n\\n// Copyright (c) 2021 BoringCrypto - All rights reserved\\n// Twitter: @Boring_Crypto\\n\\n// Special thanks to:\\n// @0xKeno - for all his invaluable contributions\\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\\n\\npragma solidity 0.6.12;\\npragma experimental ABIEncoderV2;\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/Domain.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\\\";\\nimport \\\"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\\\";\\nimport \\\"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\\\";\\nimport \\\"./interfaces/IERC721.sol\\\";\\n\\nstruct TokenLoanParams {\\n uint128 valuation; // How much will you get? OK to owe until expiration.\\n uint64 duration; // Length of loan in seconds\\n uint16 annualInterestBPS; // Variable cost of taking out the loan\\n}\\n\\ninterface ILendingClub {\\n // Per token settings.\\n function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool);\\n\\n function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory);\\n}\\n\\ninterface INFTPair {\\n function collateral() external view returns (IERC721);\\n\\n function asset() external view returns (IERC20);\\n\\n function masterContract() external view returns (address);\\n\\n function bentoBox() external view returns (IBentoBoxV1);\\n\\n function removeCollateral(uint256 tokenId, address to) external;\\n}\\n\\n/// @title NFTPair\\n/// @dev This contract allows contract calls to any contract (except BentoBox)\\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\\n using BoringMath for uint256;\\n using BoringMath128 for uint128;\\n using RebaseLibrary for Rebase;\\n using BoringERC20 for IERC20;\\n\\n event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS);\\n event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS);\\n // This automatically clears the associated loan, if any\\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\\n // Details are in the loan request\\n event LogLend(address indexed lender, uint256 indexed tokenId);\\n event LogRepay(address indexed from, uint256 indexed tokenId);\\n event LogFeeTo(address indexed newFeeTo);\\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\\n\\n // Immutables (for MasterContract and all clones)\\n IBentoBoxV1 public immutable bentoBox;\\n NFTPair public immutable masterContract;\\n\\n // MasterContract variables\\n address public feeTo;\\n\\n // Per clone variables\\n // Clone init settings\\n IERC721 public collateral;\\n IERC20 public asset;\\n\\n // A note on terminology:\\n // \\\"Shares\\\" are BentoBox shares.\\n\\n // Track assets we own. Used to allow skimming the excesss.\\n uint256 public feesEarnedShare;\\n\\n // Per token settings.\\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\\n\\n uint8 private constant LOAN_INITIAL = 0;\\n uint8 private constant LOAN_REQUESTED = 1;\\n uint8 private constant LOAN_OUTSTANDING = 2;\\n struct TokenLoan {\\n address borrower;\\n address lender;\\n uint64 startTime;\\n uint8 status;\\n }\\n mapping(uint256 => TokenLoan) public tokenLoan;\\n\\n // Do not go over 100% on either of these..\\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\\n uint256 private constant OPEN_FEE_BPS = 100;\\n uint256 private constant BPS = 10_000;\\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\\n\\n // Highest order term in the Maclaurin series for exp used by\\n // `calculateIntest`.\\n // Intuitive interpretation: interest continuously accrues on the principal.\\n // That interest, in turn, earns \\\"second-order\\\" interest-on-interest, which\\n // itself earns \\\"third-order\\\" interest, etc. This constant determines how\\n // far we take this until we stop counting.\\n //\\n // The error, in terms of the interest rate, is at least\\n //\\n // ----- n ----- Infinity\\n // \\\\ x^k \\\\ x^k\\n // e^x - ) --- , which is ) --- ,\\n // / k! / k!\\n // ----- k = 1 k ----- k = n + 1\\n //\\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\\n // interest that is owed at rate r over time t. It makes no difference if\\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\\n // is the same. Why \\\"at least\\\"? There are also rounding errors. See\\n // `calculateInterest` for more detail.\\n // The factorial in the denominator \\\"wins\\\"; for all reasonable (and quite\\n // a few unreasonable) interest rates, the lower-order terms contribute the\\n // most to the total. The following table lists some of the calculated\\n // approximations for different values of n, along with the \\\"true\\\" result:\\n //\\n // Total: 10% 20% 50% 100% 200% 500% 1000%\\n // -----------------------------------------------------------------------\\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\\n //\\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\\n //\\n // For instance, calculating the compounding effects of 200% in \\\"total\\\"\\n // interest to the sixth order results in 635.6%, whereas the true result\\n // is 638.9%.\\n // At 500% that difference is a little more dramatic, but it is still in\\n // the same ballpark -- and of little practical consequence unless the\\n // collateral can be expected to go up more than 112 times in value.\\n // Still, for volatile tokens, or an asset that is somehow known to be very\\n // inflationary, use a different number.\\n // Zero (no interest at all) is ignored and treated as one (linear only).\\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\\n\\n // For signed lend / borrow requests:\\n mapping(address => uint256) public nonces;\\n\\n /// @notice The constructor is only used for the initial master contract.\\n /// @notice Subsequent clones are initialised via `init`.\\n constructor(IBentoBoxV1 bentoBox_) public {\\n bentoBox = bentoBox_;\\n masterContract = this;\\n }\\n\\n /// @notice De facto constructor for clone contracts\\n function init(bytes calldata data) public payable override {\\n require(address(collateral) == address(0), \\\"NFTPair: already initialized\\\");\\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\\n require(address(collateral) != address(0), \\\"NFTPair: bad pair\\\");\\n }\\n\\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_OUTSTANDING) {\\n // The lender can change terms so long as the changes are strictly\\n // the same or better for the borrower:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\\n require(\\n params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS,\\n \\\"NFTPair: worse params\\\"\\n );\\n } else if (loan.status == LOAN_REQUESTED) {\\n // The borrower has already deposited the collateral and can\\n // change whatever they like\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else {\\n // The loan has not been taken out yet; the borrower needs to\\n // provide collateral.\\n revert(\\\"NFTPair: no collateral\\\");\\n }\\n tokenLoanParams[tokenId] = params;\\n emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS);\\n }\\n\\n function _requestLoan(\\n address collateralProvider,\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) private {\\n // Edge case: valuation can be zero. That effectively gifts the NFT and\\n // is therefore a bad idea, but does not break the contract.\\n require(tokenLoan[tokenId].status == LOAN_INITIAL, \\\"NFTPair: loan exists\\\");\\n if (skim) {\\n require(collateral.ownerOf(tokenId) == address(this), \\\"NFTPair: skim failed\\\");\\n } else {\\n collateral.transferFrom(collateralProvider, address(this), tokenId);\\n }\\n TokenLoan memory loan;\\n loan.borrower = to;\\n loan.status = LOAN_REQUESTED;\\n tokenLoan[tokenId] = loan;\\n tokenLoanParams[tokenId] = params;\\n\\n emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS);\\n }\\n\\n /// @notice Deposit an NFT as collateral and request a loan against it\\n /// @param tokenId ID of the NFT\\n /// @param to Address to receive the loan, or option to withdraw collateral\\n /// @param params Loan conditions on offer\\n /// @param skim True if the token has already been transfered\\n function requestLoan(\\n uint256 tokenId,\\n TokenLoanParams memory params,\\n address to,\\n bool skim\\n ) public {\\n _requestLoan(msg.sender, tokenId, params, to, skim);\\n }\\n\\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\\n /// @notice This destroys the loan.\\n /// @param tokenId The token\\n /// @param to The receiver of the token.\\n function removeCollateral(uint256 tokenId, address to) public {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n if (loan.status == LOAN_REQUESTED) {\\n // We are withdrawing collateral that is not in use:\\n require(msg.sender == loan.borrower, \\\"NFTPair: not the borrower\\\");\\n } else if (loan.status == LOAN_OUTSTANDING) {\\n // We are seizing collateral as the lender. The loan has to be\\n // expired and not paid off:\\n require(msg.sender == loan.lender, \\\"NFTPair: not the lender\\\");\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp,\\n \\\"NFTPair: not expired\\\"\\n );\\n }\\n // If there somehow is collateral but no accompanying loan, then anyone\\n // can claim it by first requesting a loan with `skim` set to true, and\\n // then withdrawing. So we might as well allow it here..\\n delete tokenLoan[tokenId];\\n collateral.transferFrom(address(this), to, tokenId);\\n emit LogRemoveCollateral(tokenId, to);\\n }\\n\\n // Assumes the lender has agreed to the loan.\\n function _lend(\\n address lender,\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) internal {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_REQUESTED, \\\"NFTPair: not available\\\");\\n TokenLoanParams memory params = tokenLoanParams[tokenId];\\n\\n // Valuation has to be an exact match, everything else must be at least\\n // as good for the lender as `accepted`.\\n require(\\n params.valuation == accepted.valuation &&\\n params.duration <= accepted.duration &&\\n params.annualInterestBPS >= accepted.annualInterestBPS,\\n \\\"NFTPair: bad params\\\"\\n );\\n\\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\\n\\n if (skim) {\\n require(\\n bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare),\\n \\\"NFTPair: skim too much\\\"\\n );\\n } else {\\n bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare);\\n }\\n // No underflow: follows from OPEN_FEE_BPS <= BPS\\n uint256 borrowerShare = totalShare - openFeeShare;\\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\\n // No overflow: addends (and result) must fit in BentoBox\\n feesEarnedShare += protocolFeeShare;\\n\\n loan.lender = lender;\\n loan.status = LOAN_OUTSTANDING;\\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\\n tokenLoan[tokenId] = loan;\\n\\n emit LogLend(lender, tokenId);\\n }\\n\\n /// @notice Lends with the parameters specified by the borrower.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param accepted Loan parameters as the lender saw them, for security\\n /// @param skim True if the funds have been transfered to the contract\\n function lend(\\n uint256 tokenId,\\n TokenLoanParams memory accepted,\\n bool skim\\n ) public {\\n _lend(msg.sender, tokenId, accepted, skim);\\n }\\n\\n // solhint-disable-next-line func-name-mixedcase\\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\\n return _domainSeparator();\\n }\\n\\n // NOTE on signature hashes: the domain separator only guarantees that the\\n // chain ID and master contract are a match, so we explicitly include the\\n // clone address (and the asset/collateral addresses):\\n\\n // keccak256(\\\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\\n\\n // keccak256(\\\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\\\")\\n bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\\n\\n /// @notice Request and immediately borrow from a pre-committed lender\\n\\n /// @notice Caller provides collateral; loan can go to a different address.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param lender Lender, whose BentoBox balance the funds will come from\\n /// @param recipient Address to receive the loan.\\n /// @param params Loan parameters requested, and signed by the lender\\n /// @param skimCollateral True if the collateral has already been transfered\\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\\n function requestAndBorrow(\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\\n require(ILendingClub(lender).willLend(tokenId, params), \\\"NFTPair: LendingClub does not like you\\\");\\n } else {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[lender]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n LEND_SIGNATURE_HASH,\\n address(this),\\n anyTokenId ? 0 : tokenId,\\n anyTokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(ecrecover(_getDigest(dataHash), v, r, s) == lender, \\\"NFTPair: signature invalid\\\");\\n }\\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\\n _lend(lender, tokenId, params, false);\\n }\\n\\n /// @notice Take collateral from a pre-commited borrower and lend against it\\n /// @notice Collateral must come from the borrower, not a third party.\\n /// @param tokenId ID of the token that will function as collateral\\n /// @param borrower Address that provides collateral and receives the loan\\n /// @param params Loan terms offered, and signed by the borrower\\n /// @param skimFunds True if the funds have been transfered to the contract\\n function takeCollateralAndLend(\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) public {\\n require(block.timestamp <= deadline, \\\"NFTPair: signature expired\\\");\\n uint256 nonce = nonces[borrower]++;\\n bytes32 dataHash = keccak256(\\n abi.encode(\\n BORROW_SIGNATURE_HASH,\\n address(this),\\n tokenId,\\n params.valuation,\\n params.duration,\\n params.annualInterestBPS,\\n nonce,\\n deadline\\n )\\n );\\n require(ecrecover(_getDigest(dataHash), v, r, s) == borrower, \\\"NFTPair: signature invalid\\\");\\n _requestLoan(borrower, tokenId, params, borrower, false);\\n _lend(msg.sender, tokenId, params, skimFunds);\\n }\\n\\n /// Approximates continuous compounding. Uses Horner's method to evaluate\\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\\n /// errors along the way. The following is always guaranteed:\\n ///\\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\\n ///\\n /// where time = t/YEAR, up to at most the rounding error obtained in\\n /// calculating linear interest.\\n ///\\n /// If the theoretical result that we are approximating (the rightmost part\\n /// of the above inquality) fits in 128 bits, then the function is\\n /// guaranteed not to revert (unless n > 250, which is way too high).\\n /// If even the linear interest (leftmost part of the inequality) does not\\n /// the function will revert.\\n /// Otherwise, the function may revert, return a reasonable result, or\\n /// return a very inaccurate result. Even then the above inequality is\\n /// respected.\\n function calculateInterest(\\n uint256 principal,\\n uint64 t,\\n uint16 aprBPS\\n ) public pure returns (uint256 interest) {\\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\\n //\\n // We calculate\\n //\\n // ----- n ----- n\\n // \\\\ principal * (t * aprBPS)^k \\\\\\n // ) -------------------------- =: ) term_k\\n // / k! * YEAR_BPS^k /\\n // ----- k = 1 ----- k = 1\\n //\\n // which approaches, but never exceeds the \\\"theoretical\\\" result,\\n //\\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\\n //\\n // as n goes to infinity. We use the fact that\\n //\\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\\n // term_k = ---------------------------------------------\\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\\n //\\n // t * aprBPS\\n // = term_{k-1} * ------------ (*)\\n // k * YEAR_BPS\\n //\\n // to calculate the terms one by one. The principal affords us the\\n // precision to carry out the division without resorting to fixed-point\\n // math. Any rounding error is downward, which we consider acceptable.\\n //\\n // Since all numbers involved are positive, each term is certainly\\n // bounded above by M. From (*) we see that any intermediate results\\n // are at most\\n //\\n // denom_k := k * YEAR_BPS.\\n //\\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\\n // which proves that all calculations will certainly not overflow if M\\n // fits in 128 bits.\\n //\\n // If M does not fit, then the intermediate results for some term may\\n // eventually overflow, but this cannot happen at the first term, and\\n // neither can the total overflow because it uses checked math.\\n //\\n // This constitutes a guarantee of specified behavior when M >= 2^128.\\n uint256 x = uint256(t) * aprBPS;\\n uint256 term_k = (principal * x) / YEAR_BPS;\\n uint256 denom_k = YEAR_BPS;\\n\\n interest = term_k;\\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\\n denom_k += YEAR_BPS;\\n term_k = (term_k * x) / denom_k;\\n interest = interest.add(term_k); // <- Only overflow check we need\\n }\\n\\n if (interest >= 2**128) {\\n revert();\\n }\\n }\\n\\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\\n TokenLoan memory loan = tokenLoan[tokenId];\\n require(loan.status == LOAN_OUTSTANDING, \\\"NFTPair: no loan\\\");\\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\\n require(\\n // Addition is safe: both summands are smaller than 256 bits\\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\\n \\\"NFTPair: loan expired\\\"\\n );\\n\\n uint128 principal = loanParams.valuation;\\n\\n // No underflow: loan.startTime is only ever set to a block timestamp\\n // Cast is safe: if this overflows, then all loans have expired anyway\\n uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128();\\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\\n amount = principal + interest;\\n\\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\\n\\n address from;\\n if (skim) {\\n require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), \\\"NFTPair: skim too much\\\");\\n from = address(this);\\n // No overflow: result fits in BentoBox\\n } else {\\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\\n from = msg.sender;\\n }\\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\\n feesEarnedShare += feeShare;\\n delete tokenLoan[tokenId];\\n\\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\\n collateral.transferFrom(address(this), loan.borrower, tokenId);\\n\\n emit LogRepay(from, tokenId);\\n }\\n\\n uint8 internal constant ACTION_REPAY = 2;\\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\\n\\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\\n uint8 internal constant ACTION_LEND = 13;\\n\\n // Function on BentoBox\\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\\n\\n // Any external call (except to BentoBox)\\n uint8 internal constant ACTION_CALL = 30;\\n\\n // Signed requests\\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\\n\\n int256 internal constant USE_VALUE1 = -1;\\n int256 internal constant USE_VALUE2 = -2;\\n\\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\\n function _num(\\n int256 inNum,\\n uint256 value1,\\n uint256 value2\\n ) internal pure returns (uint256 outNum) {\\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\\n }\\n\\n /// @dev Helper function for depositing into `bentoBox`.\\n function _bentoDeposit(\\n bytes memory data,\\n uint256 value,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\\n share = int256(_num(share, value1, value2));\\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\\n }\\n\\n /// @dev Helper function to withdraw from the `bentoBox`.\\n function _bentoWithdraw(\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (uint256, uint256) {\\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\\n }\\n\\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\\n /// This also means that calls made from this contract shall *not* be trusted.\\n function _call(\\n uint256 value,\\n bytes memory data,\\n uint256 value1,\\n uint256 value2\\n ) internal returns (bytes memory, uint8) {\\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode(\\n data,\\n (address, bytes, bool, bool, uint8)\\n );\\n\\n if (useValue1 && !useValue2) {\\n callData = abi.encodePacked(callData, value1);\\n } else if (!useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value2);\\n } else if (useValue1 && useValue2) {\\n callData = abi.encodePacked(callData, value1, value2);\\n }\\n\\n require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), \\\"NFTPair: can't call\\\");\\n\\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\\n require(success, \\\"NFTPair: call failed\\\");\\n return (returnData, returnValues);\\n }\\n\\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\\n function cook(\\n uint8[] calldata actions,\\n uint256[] calldata values,\\n bytes[] calldata datas\\n ) external payable returns (uint256 value1, uint256 value2) {\\n for (uint256 i = 0; i < actions.length; i++) {\\n uint8 action = actions[i];\\n if (action == ACTION_REPAY) {\\n (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool));\\n repay(tokenId, skim);\\n } else if (action == ACTION_REMOVE_COLLATERAL) {\\n (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address));\\n removeCollateral(tokenId, to);\\n } else if (action == ACTION_REQUEST_LOAN) {\\n (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode(\\n datas[i],\\n (uint256, TokenLoanParams, address, bool)\\n );\\n requestLoan(tokenId, params, to, skim);\\n } else if (action == ACTION_LEND) {\\n (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\\n lend(tokenId, params, skim);\\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode(\\n datas[i],\\n (address, address, bool, uint8, bytes32, bytes32)\\n );\\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\\n } else if (action == ACTION_BENTO_DEPOSIT) {\\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\\n } else if (action == ACTION_BENTO_WITHDRAW) {\\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\\n } else if (action == ACTION_BENTO_TRANSFER) {\\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\\n } else if (action == ACTION_CALL) {\\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\\n\\n if (returnValues == 1) {\\n (value1) = abi.decode(returnData, (uint256));\\n } else if (returnValues == 2) {\\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\\n }\\n } else if (action == ACTION_REQUEST_AND_BORROW) {\\n (\\n uint256 tokenId,\\n address lender,\\n address recipient,\\n TokenLoanParams memory params,\\n bool skimCollateral,\\n bool anyTokenId,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, uint256, uint8, bytes32, bytes32));\\n requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, deadline, v, r, s);\\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\\n (\\n uint256 tokenId,\\n address borrower,\\n TokenLoanParams memory params,\\n bool skimFunds,\\n uint256 deadline,\\n uint8 v,\\n bytes32 r,\\n bytes32 s\\n ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32));\\n takeCollateralAndLend(tokenId, borrower, params, skimFunds, deadline, v, r, s);\\n }\\n }\\n }\\n\\n /// @notice Withdraws the fees accumulated.\\n function withdrawFees() public {\\n address to = masterContract.feeTo();\\n\\n uint256 _share = feesEarnedShare;\\n if (_share > 0) {\\n bentoBox.transfer(asset, address(this), to, _share);\\n feesEarnedShare = 0;\\n }\\n\\n emit LogWithdrawFees(to, _share);\\n }\\n\\n /// @notice Sets the beneficiary of fees accrued in liquidations.\\n /// MasterContract Only Admin function.\\n /// @param newFeeTo The address of the receiver.\\n function setFeeTo(address newFeeTo) public onlyOwner {\\n feeTo = newFeeTo;\\n emit LogFeeTo(newFeeTo);\\n }\\n}\\n\",\"keccak256\":\"0x0ef8c4053359fb2e8ab4a574552bd5560cff87933780d76b4aeecc1d645bf22a\",\"license\":\"UNLICENSED\"},\"contracts/interfaces/IERC165.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.0 <0.8.0;\\n\\n/**\\n * @dev Interface of the ERC165 standard, as defined in the\\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\\n *\\n * Implementers can declare support of contract interfaces, which can then be\\n * queried by others ({ERC165Checker}).\\n *\\n * For an implementation, see {ERC165}.\\n */\\ninterface IERC165 {\\n /**\\n * @dev Returns true if this contract implements the interface defined by\\n * `interfaceId`. See the corresponding\\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\\n * to learn more about how these ids are created.\\n *\\n * This function call must use less than 30 000 gas.\\n */\\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\\n}\\n\",\"keccak256\":\"0x568574015c35b45a03f3bc3857240fb9985380d3faa3df7207123620d48ffe13\",\"license\":\"MIT\"},\"contracts/interfaces/IERC721.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\n// Taken from OpenZeppelin contracts v3\\n\\npragma solidity >=0.6.2 <0.8.0;\\n\\nimport \\\"./IERC165.sol\\\";\\n\\n/**\\n * @dev Required interface of an ERC721 compliant contract.\\n */\\ninterface IERC721 is IERC165 {\\n /**\\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\\n */\\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\\n */\\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\\n\\n /**\\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\\n */\\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\\n\\n /**\\n * @dev Returns the number of tokens in ``owner``'s account.\\n */\\n function balanceOf(address owner) external view returns (uint256 balance);\\n\\n /**\\n * @dev Returns the owner of the `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function ownerOf(uint256 tokenId) external view returns (address owner);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Transfers `tokenId` token from `from` to `to`.\\n *\\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n *\\n * Emits a {Transfer} event.\\n */\\n function transferFrom(address from, address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\\n * The approval is cleared when the token is transferred.\\n *\\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\\n *\\n * Requirements:\\n *\\n * - The caller must own the token or be an approved operator.\\n * - `tokenId` must exist.\\n *\\n * Emits an {Approval} event.\\n */\\n function approve(address to, uint256 tokenId) external;\\n\\n /**\\n * @dev Returns the account approved for `tokenId` token.\\n *\\n * Requirements:\\n *\\n * - `tokenId` must exist.\\n */\\n function getApproved(uint256 tokenId) external view returns (address operator);\\n\\n /**\\n * @dev Approve or remove `operator` as an operator for the caller.\\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\\n *\\n * Requirements:\\n *\\n * - The `operator` cannot be the caller.\\n *\\n * Emits an {ApprovalForAll} event.\\n */\\n function setApprovalForAll(address operator, bool _approved) external;\\n\\n /**\\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\\n *\\n * See {setApprovalForAll}\\n */\\n function isApprovedForAll(address owner, address operator) external view returns (bool);\\n\\n /**\\n * @dev Safely transfers `tokenId` token from `from` to `to`.\\n *\\n * Requirements:\\n *\\n * - `from` cannot be the zero address.\\n * - `to` cannot be the zero address.\\n * - `tokenId` token must exist and be owned by `from`.\\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\\n *\\n * Emits a {Transfer} event.\\n */\\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\\n}\\n\",\"keccak256\":\"0x0bfe878a4a6ddcdba3d5b53a21e76bcb84bc77a114fbd432a5533bda12f155fa\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x6101006040523480156200001257600080fd5b5060405162003fdf38038062003fdf8339810160408190526200003591620000fd565b600080546001600160a01b0319163390811782556040519091907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0908290a34660a08190526200008581620000a7565b608052506001600160601b0319606091821b1660c05230901b60e0526200014c565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a794692188230604051602001620000e0939291906200012d565b604051602081830303815290604052805190602001209050919050565b6000602082840312156200010f578081fd5b81516001600160a01b038116811462000126578182fd5b9392505050565b92835260208301919091526001600160a01b0316604082015260600190565b60805160a05160c05160601c60e05160601c613e02620001dd600039806108ba5280611b825250806109705280610d115280610ed95280610fb152806111d7528061160352806116af5280611764528061182b52806118f35280611f115280611fed528061219c52806126fc52806127c5528061288a528061291d525080611e75525080611eaa5250613e026000f3fe6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea2646970667358221220be96e15312f71b4b3e89cf5382353d98018daa218fd94eba4b435767c89e377f64736f6c634300060c0033", + "deployedBytecode": "0x6080604052600436106101815760003560e01c806379921557116100d1578063cd446e221161008a578063e30c397811610064578063e30c397814610410578063e7cf3f8614610425578063f41f5e1e14610445578063f46901ed1461046557610181565b8063cd446e22146103c6578063d41ddc96146103db578063d8dfeb45146103fb57610181565b806379921557146103015780637ecebe00146103215780638bea2242146103415780638da5cb5b14610371578063ba0b362314610386578063c9878e45146103a657610181565b80633644e5151161013e5780634ddf47d4116101185780634ddf47d4146102a35780634e71e0c8146102b6578063656f3d64146102cb5780636b2ace87146102ec57610181565b80633644e5151461026457806338d52e0f14610279578063476343ee1461028e57610181565b8063017e7e5814610186578063078dfbe7146101b1578063114c2cda146101d35780631329b682146102005780631b65fe041461021557806321fa310014610235575b600080fd5b34801561019257600080fd5b5061019b610485565b6040516101a89190613491565b60405180910390f35b3480156101bd57600080fd5b506101d16101cc366004612e46565b610494565b005b3480156101df57600080fd5b506101f36101ee3660046133da565b610583565b6040516101a89190613536565b34801561020c57600080fd5b506101f36105f4565b34801561022157600080fd5b506101d1610230366004613305565b6105fa565b34801561024157600080fd5b5061025561025036600461313f565b61085f565b6040516101a893929190613c44565b34801561027057600080fd5b506101f3610898565b34801561028557600080fd5b5061019b6108a7565b34801561029a57600080fd5b506101d16108b6565b6101d16102b1366004612f41565b610a28565b3480156102c257600080fd5b506101d1610aaf565b6102de6102d9366004612e90565b610b3c565b6040516101a8929190613caf565b3480156102f857600080fd5b5061019b6111d5565b34801561030d57600080fd5b506101d161031c3660046132c2565b6111f9565b34801561032d57600080fd5b506101f361033c366004612cc5565b61141a565b34801561034d57600080fd5b5061036161035c36600461313f565b61142c565b6040516101a89493929190613502565b34801561037d57600080fd5b5061019b61146f565b34801561039257600080fd5b506101f36103a13660046132e1565b61147e565b3480156103b257600080fd5b506101d16103c136600461323f565b611a1f565b3480156103d257600080fd5b5061019b611b80565b3480156103e757600080fd5b506101d16103f636600461316f565b611ba4565b34801561040757600080fd5b5061019b611d96565b34801561041c57600080fd5b5061019b611da5565b34801561043157600080fd5b506101d1610440366004613331565b611db4565b34801561045157600080fd5b506101d1610460366004613382565b611dc7565b34801561047157600080fd5b506101d1610480366004612cc5565b611dd3565b6002546001600160a01b031681565b6000546001600160a01b031633146104c75760405162461bcd60e51b81526004016104be906139ea565b60405180910390fd5b8115610562576001600160a01b0383161515806104e15750805b6104fd5760405162461bcd60e51b81526004016104be90613845565b600080546040516001600160a01b03808716939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b0385166001600160a01b03199182161790915560018054909116905561057e565b600180546001600160a01b0319166001600160a01b0385161790555b505050565b64496cebb80061ffff82166001600160401b0384160284810282900491829060025b600681116105d95764496cebb8008201915081848402816105c257fe5b0492506105cf8584611e47565b94506001016105a5565b50600160801b84106105ea57600080fd5b5050509392505050565b60055481565b610602612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff1660608201819052600214156107605780602001516001600160a01b0316336001600160a01b0316146106a25760405162461bcd60e51b81526004016104be90613ba8565b6106aa612b77565b50600083815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116838501819052600160c01b90920461ffff169483019490945291850151909216108015906107225750805183516001600160801b03918216911611155b801561073e5750806040015161ffff16836040015161ffff1611155b61075a5760405162461bcd60e51b81526004016104be90613816565b506107b6565b606081015160ff166001141561079e5780516001600160a01b031633146107995760405162461bcd60e51b81526004016104be90613ab1565b6107b6565b60405162461bcd60e51b81526004016104be90613b4a565b6000838152600660209081526040918290208451815492860151868501516001600160801b03199094166001600160801b0383161767ffffffffffffffff60801b1916600160801b6001600160401b038316021761ffff60c01b1916600160c01b61ffff86160217909255925186937fdf52f3c0981f49c8b074bb6c4ebdc7f4cdaf7ff212ac032edec0684a9cfa73ef93610852939192613c44565b60405180910390a2505050565b6006602052600090815260409020546001600160801b03811690600160801b81046001600160401b031690600160c01b900461ffff1683565b60006108a2611e70565b905090565b6004546001600160a01b031681565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663017e7e586040518163ffffffff1660e01b815260040160206040518083038186803b15801561091157600080fd5b505afa158015610925573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109499190612ce8565b60055490915080156109e35760048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936109ab9392169130918891889101613649565b600060405180830381600087803b1580156109c557600080fd5b505af11580156109d9573d6000803e3d6000fd5b5050600060055550505b816001600160a01b03167fbe641c3ffc44b2d6c184f023fa4ed7bda4b6ffa71e03b3c98ae0c776da1f17e782604051610a1c9190613536565b60405180910390a25050565b6003546001600160a01b031615610a515760405162461bcd60e51b81526004016104be90613ae8565b610a5d81830183613107565b600480546001600160a01b03199081166001600160a01b039384161790915560038054909116928216929092179182905516610aab5760405162461bcd60e51b81526004016104be90613b1f565b5050565b6001546001600160a01b0316338114610ada5760405162461bcd60e51b81526004016104be90613a4c565b600080546040516001600160a01b03808516939216917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a3600080546001600160a01b039092166001600160a01b0319928316179055600180549091169055565b60008060005b878110156111c9576000898983818110610b5857fe5b9050602002016020810190610b6d9190613410565b905060ff811660021415610bbf57600080878785818110610b8a57fe5b9050602002810190610b9c9190613cbd565b810190610ba991906132e1565b91509150610bb7828261147e565b5050506111c0565b60ff811660041415610c0e57600080878785818110610bda57fe5b9050602002810190610bec9190613cbd565b810190610bf9919061316f565b91509150610c078282611ba4565b50506111c0565b60ff8116600c1415610c6f576000610c24612b77565b600080898987818110610c3357fe5b9050602002810190610c459190613cbd565b810190610c529190613331565b9350935093509350610c6684848484611db4565b505050506111c0565b60ff8116600d1415610cc3576000610c85612b77565b6000888886818110610c9357fe5b9050602002810190610ca59190613cbd565b810190610cb29190613382565b925092509250610bb7838383611dc7565b60ff811660181415610da2576000806000806000808b8b89818110610ce457fe5b9050602002810190610cf69190613cbd565b810190610d039190612d04565b9550955095509550955095507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663c0a47c938787878787876040518763ffffffff1660e01b8152600401610d65969594939291906134a5565b600060405180830381600087803b158015610d7f57600080fd5b505af1158015610d93573d6000803e3d6000fd5b505050505050505050506111c0565b60ff811660141415610e2a57610e20868684818110610dbd57fe5b9050602002810190610dcf9190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508c92508b9150869050818110610e1257fe5b905060200201358686611ed0565b90945092506111c0565b60ff811660151415610e9557610e20868684818110610e4557fe5b9050602002810190610e579190613cbd565b8080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250889250879150611fc69050565b60ff811660161415610f6d576000806000888886818110610eb257fe5b9050602002810190610ec49190613cbd565b810190610ed19190612fad565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663f18d03cc843385610f14868d8d6120b4565b6040518563ffffffff1660e01b8152600401610f339493929190613649565b600060405180830381600087803b158015610f4d57600080fd5b505af1158015610f61573d6000803e3d6000fd5b505050505050506111c0565b60ff811660171415611001576000606080888886818110610f8a57fe5b9050602002810190610f9c9190613cbd565b810190610fa99190613034565b9250925092507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316630fca8843843385856040518563ffffffff1660e01b8152600401610f3394939291906136a7565b60ff8116601e14156110da57606060006110838a8a8681811061102057fe5b9050602002013589898781811061103357fe5b90506020028101906110459190613cbd565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152508b92508a91506120de9050565b915091508060ff16600114156110ae57818060200190518101906110a79190613157565b9550610c07565b8060ff1660021415610c0757818060200190518101906110ce91906133b7565b909650945050506111c0565b60ff81166028141561114d5760008060006110f3612b77565b6000806000806000808f8f8d81811061110857fe5b905060200281019061111a9190613cbd565b8101906111279190613193565b9950995099509950995099509950995099509950610d938a8a8a8a8a8a8a8a8a8a6111f9565b60ff8116602914156111c057600080611164612b77565b60008060008060008d8d8b81811061117857fe5b905060200281019061118a9190613cbd565b810190611197919061323f565b975097509750975097509750975097506111b78888888888888888611a1f565b50505050505050505b50600101610b42565b50965096945050505050565b7f000000000000000000000000000000000000000000000000000000000000000081565b60ff8316158015611208575081155b8015611212575080155b156112b657604051630960450960e11b81526001600160a01b038a16906312c08a1290611245908d908b90600401613c72565b60206040518083038186803b15801561125d57600080fd5b505afa158015611271573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906112959190612f25565b6112b15760405162461bcd60e51b81526004016104be906139a4565b6113f4565b834211156112d65760405162461bcd60e51b81526004016104be90613874565b6001600160a01b0389166000908152600860205260408120805460018101909155907f06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f83088611325578d611328565b60005b898c600001518d602001518e60400151888d6040516020016113529998979695949392919061353f565b6040516020818303038152906040528051906020012090508a6001600160a01b0316600161137f836122ae565b8787876040516000815260200160405260405161139f9493929190613611565b6020604051602081039080840390855afa1580156113c1573d6000803e3d6000fd5b505050602060405103516001600160a01b0316146113f15760405162461bcd60e51b81526004016104be90613c0d565b50505b611401338b898b8a612303565b61140e898b896000612593565b50505050505050505050565b60086020526000908152604090205481565b600760205260009081526040902080546001909101546001600160a01b0391821691811690600160a01b81046001600160401b031690600160e01b900460ff1684565b6000546001600160a01b031681565b6000611488612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019092015491821692810192909252600160a01b81046001600160401b031692820192909252600160e01b90910460ff166060820181905260021461150a5760405162461bcd60e51b81526004016104be9061378f565b611512612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b0381168252600160801b81046001600160401b03908116938301849052600160c01b90910461ffff16828501529284015190924291169091011161158c5760405162461bcd60e51b81526004016104be90613910565b60008160000151905060006115c66115c1836001600160801b031686604001516001600160401b031642038660400151610583565b612aec565b60048054604051636d289ce560e11b81526001600160801b03938416938616840198509293506127106103e8850204926000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca9361163e9392909116918c9187910161376c565b60206040518083038186803b15801561165657600080fd5b505afa15801561166a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061168e9190613157565b60048054604051636d289ce560e11b81529293506000926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363da5139ca936116e893921691889187910161376c565b60206040518083038186803b15801561170057600080fd5b505afa158015611714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117389190613157565b9050600089156118105760055460048054604051633de222bb60e21b8152928601926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec9361179b9392169130910161362f565b60206040518083038186803b1580156117b357600080fd5b505afa1580156117c7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906117eb9190613157565b10156118095760405162461bcd60e51b81526004016104be90613a81565b503061189c565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936118669392169133913091899101613649565b600060405180830381600087803b15801561188057600080fd5b505af1158015611894573d6000803e3d6000fd5b505050503390505b600580548301905560008b81526007602090815260409182902080546001600160a01b031916815560010180546001600160e81b031916905560048054918b01519251633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc9461192e949216928792898b039101613649565b600060405180830381600087803b15801561194857600080fd5b505af115801561195c573d6000803e3d6000fd5b50505050600360009054906101000a90046001600160a01b03166001600160a01b03166323b872dd308a600001518e6040518463ffffffff1660e01b81526004016119a9939291906134de565b600060405180830381600087803b1580156119c357600080fd5b505af11580156119d7573d6000803e3d6000fd5b50506040518d92506001600160a01b03841691507fcd300581542c5eab58e736a0b08b42cec829c4504d1c16af90f4630b27e30de390600090a3505050505050505092915050565b83421115611a3f5760405162461bcd60e51b81526004016104be90613874565b600060086000896001600160a01b03166001600160a01b03168152602001908152602001600020600081548092919060010191905055905060007ff2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec433660001b308b8a600001518b602001518c60400151878c604051602001611ac798979695949392919061359d565b604051602081830303815290604052805190602001209050886001600160a01b03166001611af4836122ae565b87878760405160008152602001604052604051611b149493929190613611565b6020604051602081039080840390855afa158015611b36573d6000803e3d6000fd5b505050602060405103516001600160a01b031614611b665760405162461bcd60e51b81526004016104be90613c0d565b611b74898b8a8c6000612303565b61140e338b8a8a612593565b7f000000000000000000000000000000000000000000000000000000000000000081565b611bac612b50565b50600082815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521415611c435780516001600160a01b03163314611c3e5760405162461bcd60e51b81526004016104be90613ab1565b611cd2565b606081015160ff1660021415611cd25780602001516001600160a01b0316336001600160a01b031614611c885760405162461bcd60e51b81526004016104be90613ba8565b60008381526006602052604090819020549082015142600160801b9092046001600160401b039081169116011115611cd25760405162461bcd60e51b81526004016104be90613bdf565b6000838152600760205260409081902080546001600160a01b031916815560010180546001600160e81b031916905560035490516323b872dd60e01b81526001600160a01b03909116906323b872dd90611d34903090869088906004016134de565b600060405180830381600087803b158015611d4e57600080fd5b505af1158015611d62573d6000803e3d6000fd5b50505050827f279c10f9827cdddd314534dd33cb906c270c3ac21cdd72ed94a1d534aca5a25a836040516108529190613491565b6003546001600160a01b031681565b6001546001600160a01b031681565b611dc13385858585612303565b50505050565b61057e33848484612593565b6000546001600160a01b03163314611dfd5760405162461bcd60e51b81526004016104be906139ea565b600280546001600160a01b0319166001600160a01b0383169081179091556040517fcf1d3f17e521c635e0d20b8acba94ba170afc041d0546d46dafa09d3c9c19eb390600090a250565b81810181811015611e6a5760405162461bcd60e51b81526004016104be9061393f565b92915050565b6000467f00000000000000000000000000000000000000000000000000000000000000008114611ea857611ea381612b19565b611eca565b7f00000000000000000000000000000000000000000000000000000000000000005b91505090565b60008060008060008089806020019051810190611eed9190612fed565b9350935093509350611f008289896120b4565b9150611f0d8189896120b4565b90507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166302b9446c8a86338787876040518763ffffffff1660e01b8152600401611f64959493929190613673565b60408051808303818588803b158015611f7c57600080fd5b505af1158015611f90573d6000803e3d6000fd5b50505050506040513d601f19601f82011682018060405250810190611fb591906133b7565b955095505050505094509492505050565b60008060008060008088806020019051810190611fe39190612fed565b93509350935093507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166397da6d30853386612028878e8e6120b4565b612033878f8f6120b4565b6040518663ffffffff1660e01b8152600401612053959493929190613673565b6040805180830381600087803b15801561206c57600080fd5b505af1158015612080573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120a491906133b7565b9550955050505050935093915050565b6000808412156120d45760001984146120cd57816120cf565b825b6120d6565b835b949350505050565b606060008060606000806000898060200190518101906120fe9190612d71565b94509450945094509450828015612113575081155b1561214157838960405160200161212b929190613448565b604051602081830303815290604052935061219a565b8215801561214c5750815b1561216457838860405160200161212b929190613448565b82801561216e5750815b1561219a578389896040516020016121889392919061346a565b60405160208183030381529060405293505b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316856001600160a01b0316141580156121ea57506003546001600160a01b03868116911614155b80156121ff57506001600160a01b0385163014155b61221b5760405162461bcd60e51b81526004016104be90613a1f565b60006060866001600160a01b03168d87604051612238919061342c565b60006040518083038185875af1925050503d8060008114612275576040519150601f19603f3d011682016040523d82523d6000602084013e61227a565b606091505b50915091508161229c5760405162461bcd60e51b81526004016104be906138ab565b9c919b50909950505050505050505050565b600060405180604001604052806002815260200161190160f01b8152506122d3611e70565b836040516020016122e69392919061346a565b604051602081830303815290604052805190602001209050919050565b600084815260076020526040902060010154600160e01b900460ff161561233c5760405162461bcd60e51b81526004016104be90613976565b80156123ed576003546040516331a9108f60e11b815230916001600160a01b031690636352211e90612372908890600401613536565b60206040518083038186803b15801561238a57600080fd5b505afa15801561239e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906123c29190612ce8565b6001600160a01b0316146123e85760405162461bcd60e51b81526004016104be90613b7a565b612454565b6003546040516323b872dd60e01b81526001600160a01b03909116906323b872dd90612421908890309089906004016134de565b600060405180830381600087803b15801561243b57600080fd5b505af115801561244f573d6000803e3d6000fd5b505050505b61245c612b50565b6001600160a01b038381168083526001606084018181526000898152600760209081526040808320885181546001600160a01b0319908116918a16919091178255838a0151919096018054838b015196519716919098161767ffffffffffffffff60a01b1916600160a01b6001600160401b03958616021760ff60e01b1916600160e01b60ff90961695909502949094179095556006855282902088518154958a01518a8501516001600160801b03199097166001600160801b0383161767ffffffffffffffff60801b1916600160801b948216949094029390931761ffff60c01b1916600160c01b61ffff88160217909155915189947f37067dab1c05118bd00db86de14fcd009c2a6109392037ade66d33f8f6bcd17393612583939092909190613c44565b60405180910390a3505050505050565b61259b612b50565b50600083815260076020908152604091829020825160808101845281546001600160a01b03908116825260019283015490811693820193909352600160a01b83046001600160401b031693810193909352600160e01b90910460ff16606083018190521461261b5760405162461bcd60e51b81526004016104be906137b9565b612623612b77565b50600084815260066020908152604091829020825160608101845290546001600160801b03808216808452600160801b83046001600160401b031694840194909452600160c01b90910461ffff169382019390935285519092161480156126a4575083602001516001600160401b031681602001516001600160401b031611155b80156126c05750836040015161ffff16816040015161ffff1610155b6126dc5760405162461bcd60e51b81526004016104be906137e9565b600480548251604051636d289ce560e11b81526000936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463da5139ca94612735949190921692879101613740565b60206040518083038186803b15801561274d57600080fd5b505afa158015612761573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127859190613157565b905061271060648202819004906103e8820204851561286f5760055460048054604051633de222bb60e21b81528587038501909301926001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f7888aec936127fc9392169130910161362f565b60206040518083038186803b15801561281457600080fd5b505afa158015612828573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061284c9190613157565b101561286a5760405162461bcd60e51b81526004016104be90613a81565b6128fc565b60048054604051633c6340f360e21b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169363f18d03cc936128c9939216918e913091898b0389019101613649565b600060405180830381600087803b1580156128e357600080fd5b505af11580156128f7573d6000803e3d6000fd5b505050505b600480548651604051633c6340f360e21b8152858703936001600160a01b037f000000000000000000000000000000000000000000000000000000000000000081169463f18d03cc94612959949190921692309291889101613649565b600060405180830381600087803b15801561297357600080fd5b505af1158015612987573d6000803e3d6000fd5b50505050816005600082825401925050819055508986602001906001600160a01b031690816001600160a01b0316815250506002866060019060ff16908160ff16815250504286604001906001600160401b031690816001600160401b03168152505085600760008b815260200190815260200160002060008201518160000160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060208201518160010160006101000a8154816001600160a01b0302191690836001600160a01b0316021790555060408201518160010160146101000a8154816001600160401b0302191690836001600160401b03160217905550606082015181600101601c6101000a81548160ff021916908360ff160217905550905050888a6001600160a01b03167ff0742e8f1b967b4a34ebd6094f10a23dd802856a1591ee09b37c06df665ec18e60405160405180910390a350505050505050505050565b60006001600160801b03821115612b155760405162461bcd60e51b81526004016104be906138d9565b5090565b60007f47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a7946921882306040516020016122e6939291906135f2565b60408051608081018252600080825260208201819052918101829052606081019190915290565b604080516060810182526000808252602082018190529181019190915290565b60008083601f840112612ba8578182fd5b5081356001600160401b03811115612bbe578182fd5b6020830191508360208083028501011115612bd857600080fd5b9250929050565b600082601f830112612bef578081fd5b8135612c02612bfd82613d27565b613d01565b818152915060208083019084810181840286018201871015612c2357600080fd5b60005b84811015612c4257813584529282019290820190600101612c26565b505050505092915050565b8051611e6a81613d8a565b600060608284031215612c69578081fd5b612c736060613d01565b905081356001600160801b0381168114612c8c57600080fd5b81526020820135612c9c81613da8565b60208201526040820135612caf81613d98565b604082015292915050565b8051611e6a81613dbd565b600060208284031215612cd6578081fd5b8135612ce181613d72565b9392505050565b600060208284031215612cf9578081fd5b8151612ce181613d72565b60008060008060008060c08789031215612d1c578182fd5b8635612d2781613d72565b95506020870135612d3781613d72565b94506040870135612d4781613d8a565b93506060870135612d5781613dbd565b9598949750929560808101359460a0909101359350915050565b600080600080600060a08688031215612d88578283fd5b8551612d9381613d72565b60208701519095506001600160401b0380821115612daf578485fd5b818801915088601f830112612dc2578485fd5b815181811115612dd0578586fd5b612de3601f8201601f1916602001613d01565b9150808252896020828501011115612df9578586fd5b612e0a816020840160208601613d46565b509450612e1c90508760408801612c4d565b9250612e2b8760608801612c4d565b9150612e3a8760808801612cba565b90509295509295909350565b600080600060608486031215612e5a578081fd5b8335612e6581613d72565b92506020840135612e7581613d8a565b91506040840135612e8581613d8a565b809150509250925092565b60008060008060008060608789031215612ea8578384fd5b86356001600160401b0380821115612ebe578586fd5b612eca8a838b01612b97565b90985096506020890135915080821115612ee2578586fd5b612eee8a838b01612b97565b90965094506040890135915080821115612f06578384fd5b50612f1389828a01612b97565b979a9699509497509295939492505050565b600060208284031215612f36578081fd5b8151612ce181613d8a565b60008060208385031215612f53578182fd5b82356001600160401b0380821115612f69578384fd5b818501915085601f830112612f7c578384fd5b813581811115612f8a578485fd5b866020828501011115612f9b578485fd5b60209290920196919550909350505050565b600080600060608486031215612fc1578081fd5b8335612fcc81613d72565b92506020840135612fdc81613d72565b929592945050506040919091013590565b60008060008060808587031215613002578182fd5b845161300d81613d72565b602086015190945061301e81613d72565b6040860151606090960151949790965092505050565b600080600060608486031215613048578081fd5b833561305381613d72565b92506020848101356001600160401b038082111561306f578384fd5b818701915087601f830112613082578384fd5b8135613090612bfd82613d27565b81815284810190848601868402860187018c10156130ac578788fd5b8795505b838610156130d75780356130c381613d72565b8352600195909501949186019186016130b0565b509650505060408701359250808311156130ef578384fd5b50506130fd86828701612bdf565b9150509250925092565b60008060408385031215613119578182fd5b823561312481613d72565b9150602083013561313481613d72565b809150509250929050565b600060208284031215613150578081fd5b5035919050565b600060208284031215613168578081fd5b5051919050565b60008060408385031215613181578182fd5b82359150602083013561313481613d72565b6000806000806000806000806000806101808b8d0312156131b2578788fd5b8a35995060208b01356131c481613d72565b985060408b01356131d481613d72565b97506131e38c60608d01612c58565b965060c08b01356131f381613d8a565b955060e08b013561320381613d8a565b94506101008b013593506101208b013561321c81613dbd565b809350506101408b013591506101608b013590509295989b9194979a5092959850565b600080600080600080600080610140898b03121561325b578182fd5b88359750602089013561326d81613d72565b965061327c8a60408b01612c58565b955060a089013561328c81613d8a565b945060c0890135935060e08901356132a381613dbd565b979a969950949793969295929450505061010082013591610120013590565b6000806000806000806000806000806101808b8d0312156131b2578384fd5b600080604083850312156132f3578182fd5b82359150602083013561313481613d8a565b60008060808385031215613317578182fd5b823591506133288460208501612c58565b90509250929050565b60008060008060c08587031215613346578182fd5b843593506133578660208701612c58565b9250608085013561336781613d72565b915060a085013561337781613d8a565b939692955090935050565b600080600060a08486031215613396578081fd5b833592506133a78560208601612c58565b91506080840135612e8581613d8a565b600080604083850312156133c9578182fd5b505080516020909101519092909150565b6000806000606084860312156133ee578081fd5b83359250602084013561340081613da8565b91506040840135612e8581613d98565b600060208284031215613421578081fd5b8135612ce181613dbd565b6000825161343e818460208701613d46565b9190910192915050565b6000835161345a818460208801613d46565b9190910191825250602001919050565b6000845161347c818460208901613d46565b91909101928352506020820152604001919050565b6001600160a01b0391909116815260200190565b6001600160a01b039687168152949095166020850152911515604084015260ff166060830152608082015260a081019190915260c00190565b6001600160a01b039384168152919092166020820152604081019190915260600190565b6001600160a01b0394851681529290931660208301526001600160401b0316604082015260ff909116606082015260800190565b90815260200190565b9889526001600160a01b03979097166020890152604088019590955292151560608701526001600160801b039190911660808601526001600160401b031660a085015261ffff1660c084015260e08301526101008201526101200190565b9788526001600160a01b0396909616602088015260408701949094526001600160801b039290921660608601526001600160401b0316608085015261ffff1660a084015260c083015260e08201526101000190565b92835260208301919091526001600160a01b0316604082015260600190565b93845260ff9290921660208401526040830152606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b039485168152928416602084015292166040820152606081019190915260800190565b6001600160a01b03958616815293851660208501529190931660408301526060820192909252608081019190915260a00190565b60006080820160018060a01b0380881684526020818816818601526080604086015282875180855260a0870191508289019450855b818110156136fa5785518516835294830194918301916001016136dc565b50508581036060870152865180825290820193509150808601845b8381101561373157815185529382019390820190600101613715565b50929998505050505050505050565b6001600160a01b039390931683526001600160801b039190911660208301521515604082015260600190565b6001600160a01b0393909316835260208301919091521515604082015260600190565b60208082526010908201526f27232a2830b4b91d103737903637b0b760811b604082015260600190565b6020808252601690820152754e4654506169723a206e6f7420617661696c61626c6560501b604082015260600190565b6020808252601390820152724e4654506169723a2062616420706172616d7360681b604082015260600190565b6020808252601590820152744e4654506169723a20776f72736520706172616d7360581b604082015260600190565b6020808252601590820152744f776e61626c653a207a65726f206164647265737360581b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e61747572652065787069726564000000000000604082015260600190565b60208082526014908201527313919514185a5c8e8818d85b1b0819985a5b195960621b604082015260600190565b6020808252601c908201527f426f72696e674d6174683a2075696e74313238204f766572666c6f7700000000604082015260600190565b60208082526015908201527413919514185a5c8e881b1bd85b88195e1c1a5c9959605a1b604082015260600190565b60208082526018908201527f426f72696e674d6174683a20416464204f766572666c6f770000000000000000604082015260600190565b6020808252601490820152734e4654506169723a206c6f616e2065786973747360601b604082015260600190565b60208082526026908201527f4e4654506169723a204c656e64696e67436c756220646f6573206e6f74206c696040820152656b6520796f7560d01b606082015260800190565b6020808252818101527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604082015260600190565b60208082526013908201527213919514185a5c8e8818d85b89dd0818d85b1b606a1b604082015260600190565b6020808252818101527f4f776e61626c653a2063616c6c657220213d2070656e64696e67206f776e6572604082015260600190565b60208082526016908201527509c8ca8a0c2d2e47440e6d6d2da40e8dede40daeac6d60531b604082015260600190565b60208082526019908201527f4e4654506169723a206e6f742074686520626f72726f77657200000000000000604082015260600190565b6020808252601c908201527f4e4654506169723a20616c726561647920696e697469616c697a656400000000604082015260600190565b60208082526011908201527027232a2830b4b91d103130b2103830b4b960791b604082015260600190565b60208082526016908201527513919514185a5c8e881b9bc818dbdb1b185d195c985b60521b604082015260600190565b60208082526014908201527313919514185a5c8e881cdada5b4819985a5b195960621b604082015260600190565b60208082526017908201527f4e4654506169723a206e6f7420746865206c656e646572000000000000000000604082015260600190565b60208082526014908201527313919514185a5c8e881b9bdd08195e1c1a5c995960621b604082015260600190565b6020808252601a908201527f4e4654506169723a207369676e617475726520696e76616c6964000000000000604082015260600190565b6001600160801b039390931683526001600160401b0391909116602083015261ffff16604082015260600190565b91825280516001600160801b03166020808401919091528101516001600160401b0316604080840191909152015161ffff16606082015260800190565b918252602082015260400190565b6000808335601e19843603018112613cd3578283fd5b8301803591506001600160401b03821115613cec578283fd5b602001915036819003821315612bd857600080fd5b6040518181016001600160401b0381118282101715613d1f57600080fd5b604052919050565b60006001600160401b03821115613d3c578081fd5b5060209081020190565b60005b83811015613d61578181015183820152602001613d49565b83811115611dc15750506000910152565b6001600160a01b0381168114613d8757600080fd5b50565b8015158114613d8757600080fd5b61ffff81168114613d8757600080fd5b6001600160401b0381168114613d8757600080fd5b60ff81168114613d8757600080fdfea2646970667358221220be96e15312f71b4b3e89cf5382353d98018daa218fd94eba4b435767c89e377f64736f6c634300060c0033", "devdoc": { "details": "This contract allows contract calls to any contract (except BentoBox) from arbitrary callers thus, don't trust calls from this contract in any circumstances.", "kind": "dev", @@ -986,7 +986,7 @@ "type": "t_address" }, { - "astId": 12042, + "astId": 14561, "contract": "contracts/NFTPair.sol:NFTPair", "label": "feeTo", "offset": 0, @@ -994,15 +994,15 @@ "type": "t_address" }, { - "astId": 12044, + "astId": 14563, "contract": "contracts/NFTPair.sol:NFTPair", "label": "collateral", "offset": 0, "slot": "3", - "type": "t_contract(IERC721)17337" + "type": "t_contract(IERC721)19856" }, { - "astId": 12046, + "astId": 14565, "contract": "contracts/NFTPair.sol:NFTPair", "label": "asset", "offset": 0, @@ -1010,7 +1010,7 @@ "type": "t_contract(IERC20)1405" }, { - "astId": 12048, + "astId": 14567, "contract": "contracts/NFTPair.sol:NFTPair", "label": "feesEarnedShare", "offset": 0, @@ -1018,23 +1018,23 @@ "type": "t_uint256" }, { - "astId": 12052, + "astId": 14571, "contract": "contracts/NFTPair.sol:NFTPair", "label": "tokenLoanParams", "offset": 0, "slot": "6", - "type": "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)" + "type": "t_mapping(t_uint256,t_struct(TokenLoanParams)14439_storage)" }, { - "astId": 12074, + "astId": 14593, "contract": "contracts/NFTPair.sol:NFTPair", "label": "tokenLoan", "offset": 0, "slot": "7", - "type": "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)" + "type": "t_mapping(t_uint256,t_struct(TokenLoan)14589_storage)" }, { - "astId": 12099, + "astId": 14618, "contract": "contracts/NFTPair.sol:NFTPair", "label": "nonces", "offset": 0, @@ -1053,7 +1053,7 @@ "label": "contract IERC20", "numberOfBytes": "20" }, - "t_contract(IERC721)17337": { + "t_contract(IERC721)19856": { "encoding": "inplace", "label": "contract IERC721", "numberOfBytes": "20" @@ -1065,26 +1065,26 @@ "numberOfBytes": "32", "value": "t_uint256" }, - "t_mapping(t_uint256,t_struct(TokenLoan)12070_storage)": { + "t_mapping(t_uint256,t_struct(TokenLoan)14589_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct NFTPair.TokenLoan)", "numberOfBytes": "32", - "value": "t_struct(TokenLoan)12070_storage" + "value": "t_struct(TokenLoan)14589_storage" }, - "t_mapping(t_uint256,t_struct(TokenLoanParams)11920_storage)": { + "t_mapping(t_uint256,t_struct(TokenLoanParams)14439_storage)": { "encoding": "mapping", "key": "t_uint256", "label": "mapping(uint256 => struct TokenLoanParams)", "numberOfBytes": "32", - "value": "t_struct(TokenLoanParams)11920_storage" + "value": "t_struct(TokenLoanParams)14439_storage" }, - "t_struct(TokenLoan)12070_storage": { + "t_struct(TokenLoan)14589_storage": { "encoding": "inplace", "label": "struct NFTPair.TokenLoan", "members": [ { - "astId": 12063, + "astId": 14582, "contract": "contracts/NFTPair.sol:NFTPair", "label": "borrower", "offset": 0, @@ -1092,7 +1092,7 @@ "type": "t_address" }, { - "astId": 12065, + "astId": 14584, "contract": "contracts/NFTPair.sol:NFTPair", "label": "lender", "offset": 0, @@ -1100,7 +1100,7 @@ "type": "t_address" }, { - "astId": 12067, + "astId": 14586, "contract": "contracts/NFTPair.sol:NFTPair", "label": "startTime", "offset": 20, @@ -1108,7 +1108,7 @@ "type": "t_uint64" }, { - "astId": 12069, + "astId": 14588, "contract": "contracts/NFTPair.sol:NFTPair", "label": "status", "offset": 28, @@ -1118,12 +1118,12 @@ ], "numberOfBytes": "64" }, - "t_struct(TokenLoanParams)11920_storage": { + "t_struct(TokenLoanParams)14439_storage": { "encoding": "inplace", "label": "struct TokenLoanParams", "members": [ { - "astId": 11915, + "astId": 14434, "contract": "contracts/NFTPair.sol:NFTPair", "label": "valuation", "offset": 0, @@ -1131,7 +1131,7 @@ "type": "t_uint128" }, { - "astId": 11917, + "astId": 14436, "contract": "contracts/NFTPair.sol:NFTPair", "label": "duration", "offset": 16, @@ -1139,7 +1139,7 @@ "type": "t_uint64" }, { - "astId": 11919, + "astId": 14438, "contract": "contracts/NFTPair.sol:NFTPair", "label": "annualInterestBPS", "offset": 24, diff --git a/deployments/ropsten/solcInputs/495ac39dee8145a05ebd7dff96081707.json b/deployments/ropsten/solcInputs/495ac39dee8145a05ebd7dff96081707.json new file mode 100644 index 00000000..61328daa --- /dev/null +++ b/deployments/ropsten/solcInputs/495ac39dee8145a05ebd7dff96081707.json @@ -0,0 +1,371 @@ +{ + "language": "Solidity", + "sources": { + "contracts/Cauldron.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract Cauldron is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n Cauldron public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 private constant INTEREST_PER_SECOND = 317097920;\r\n\r\n uint256 private constant COLLATERIZATION_RATE = 75000; // 75%\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 private constant LIQUIDATION_MULTIPLIER = 112000; // add 12%\r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 private constant BORROW_OPENING_FEE = 50; // 0.05%\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData) = abi.decode(data, (IERC20, IOracle, bytes));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\r\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\r\n }\r\n}\r\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\n/// @notice A library for performing overflow-/underflow-safe math,\n/// updated with awesomeness from of DappHub (https://github.com/dapphub/ds-math).\nlibrary BoringMath {\n function add(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n\n function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {\n require(b == 0 || (c = a * b) / b == a, \"BoringMath: Mul Overflow\");\n }\n\n function to128(uint256 a) internal pure returns (uint128 c) {\n require(a <= uint128(-1), \"BoringMath: uint128 Overflow\");\n c = uint128(a);\n }\n\n function to64(uint256 a) internal pure returns (uint64 c) {\n require(a <= uint64(-1), \"BoringMath: uint64 Overflow\");\n c = uint64(a);\n }\n\n function to32(uint256 a) internal pure returns (uint32 c) {\n require(a <= uint32(-1), \"BoringMath: uint32 Overflow\");\n c = uint32(a);\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint128.\nlibrary BoringMath128 {\n function add(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint128 a, uint128 b) internal pure returns (uint128 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint64.\nlibrary BoringMath64 {\n function add(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint64 a, uint64 b) internal pure returns (uint64 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n\n/// @notice A library for performing overflow-/underflow-safe addition and subtraction on uint32.\nlibrary BoringMath32 {\n function add(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a + b) >= b, \"BoringMath: Add Overflow\");\n }\n\n function sub(uint32 a, uint32 b) internal pure returns (uint32 c) {\n require((c = a - b) <= a, \"BoringMath: Underflow\");\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\n// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol + Claimable.sol\n// Edited by BoringCrypto\n\ncontract BoringOwnableData {\n address public owner;\n address public pendingOwner;\n}\n\ncontract BoringOwnable is BoringOwnableData {\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /// @notice `owner` defaults to msg.sender on construction.\n constructor() public {\n owner = msg.sender;\n emit OwnershipTransferred(address(0), msg.sender);\n }\n\n /// @notice Transfers ownership to `newOwner`. Either directly or claimable by the new pending owner.\n /// Can only be invoked by the current `owner`.\n /// @param newOwner Address of the new owner.\n /// @param direct True if `newOwner` should be set immediately. False if `newOwner` needs to use `claimOwnership`.\n /// @param renounce Allows the `newOwner` to be `address(0)` if `direct` and `renounce` is True. Has no effect otherwise.\n function transferOwnership(\n address newOwner,\n bool direct,\n bool renounce\n ) public onlyOwner {\n if (direct) {\n // Checks\n require(newOwner != address(0) || renounce, \"Ownable: zero address\");\n\n // Effects\n emit OwnershipTransferred(owner, newOwner);\n owner = newOwner;\n pendingOwner = address(0);\n } else {\n // Effects\n pendingOwner = newOwner;\n }\n }\n\n /// @notice Needs to be called by `pendingOwner` to claim ownership.\n function claimOwnership() public {\n address _pendingOwner = pendingOwner;\n\n // Checks\n require(msg.sender == _pendingOwner, \"Ownable: caller != pending owner\");\n\n // Effects\n emit OwnershipTransferred(owner, _pendingOwner);\n owner = _pendingOwner;\n pendingOwner = address(0);\n }\n\n /// @notice Only allows the `owner` to execute the function.\n modifier onlyOwner() {\n require(msg.sender == owner, \"Ownable: caller is not the owner\");\n _;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/ERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"./interfaces/IERC20.sol\";\nimport \"./Domain.sol\";\n\n// solhint-disable no-inline-assembly\n// solhint-disable not-rely-on-time\n\n// Data part taken out for building of contracts that receive delegate calls\ncontract ERC20Data {\n /// @notice owner > balance mapping.\n mapping(address => uint256) public balanceOf;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n}\n\nabstract contract ERC20 is IERC20, Domain {\n /// @notice owner > balance mapping.\n mapping(address => uint256) public override balanceOf;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public override allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n\n /// @notice Transfers `amount` tokens from `msg.sender` to `to`.\n /// @param to The address to move the tokens.\n /// @param amount of the tokens to move.\n /// @return (bool) Returns True if succeeded.\n function transfer(address to, uint256 amount) public returns (bool) {\n // If `amount` is 0, or `msg.sender` is `to` nothing happens\n if (amount != 0 || msg.sender == to) {\n uint256 srcBalance = balanceOf[msg.sender];\n require(srcBalance >= amount, \"ERC20: balance too low\");\n if (msg.sender != to) {\n require(to != address(0), \"ERC20: no zero address\"); // Moved down so low balance calls safe some gas\n\n balanceOf[msg.sender] = srcBalance - amount; // Underflow is checked\n balanceOf[to] += amount;\n }\n }\n emit Transfer(msg.sender, to, amount);\n return true;\n }\n\n /// @notice Transfers `amount` tokens from `from` to `to`. Caller needs approval for `from`.\n /// @param from Address to draw tokens from.\n /// @param to The address to move the tokens.\n /// @param amount The token amount to move.\n /// @return (bool) Returns True if succeeded.\n function transferFrom(\n address from,\n address to,\n uint256 amount\n ) public returns (bool) {\n // If `amount` is 0, or `from` is `to` nothing happens\n if (amount != 0) {\n uint256 srcBalance = balanceOf[from];\n require(srcBalance >= amount, \"ERC20: balance too low\");\n\n if (from != to) {\n uint256 spenderAllowance = allowance[from][msg.sender];\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\n if (spenderAllowance != type(uint256).max) {\n require(spenderAllowance >= amount, \"ERC20: allowance too low\");\n allowance[from][msg.sender] = spenderAllowance - amount; // Underflow is checked\n }\n require(to != address(0), \"ERC20: no zero address\"); // Moved down so other failed calls safe some gas\n\n balanceOf[from] = srcBalance - amount; // Underflow is checked\n balanceOf[to] += amount;\n }\n }\n emit Transfer(from, to, amount);\n return true;\n }\n\n /// @notice Approves `amount` from sender to be spend by `spender`.\n /// @param spender Address of the party that can draw from msg.sender's account.\n /// @param amount The maximum collective amount that `spender` can draw.\n /// @return (bool) Returns True if approved.\n function approve(address spender, uint256 amount) public override returns (bool) {\n allowance[msg.sender][spender] = amount;\n emit Approval(msg.sender, spender, amount);\n return true;\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\n\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\n /// @param owner_ Address of the owner.\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\n /// @param value The maximum collective amount that `spender` can draw.\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override {\n require(owner_ != address(0), \"ERC20: Owner cannot be 0\");\n require(block.timestamp < deadline, \"ERC20: Expired\");\n require(\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\n owner_,\n \"ERC20: Invalid Signature\"\n );\n allowance[owner_][spender] = value;\n emit Approval(owner_, spender, value);\n }\n}\n\ncontract ERC20WithSupply is IERC20, ERC20 {\n uint256 public override totalSupply;\n\n function _mint(address user, uint256 amount) internal {\n uint256 newTotalSupply = totalSupply + amount;\n require(newTotalSupply >= totalSupply, \"Mint overflow\");\n totalSupply = newTotalSupply;\n balanceOf[user] += amount;\n emit Transfer(address(0), user, amount);\n }\n\n function _burn(address user, uint256 amount) internal {\n require(balanceOf[user] >= amount, \"Burn too much\");\n totalSupply -= amount;\n balanceOf[user] -= amount;\n emit Transfer(user, address(0), amount);\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IMasterContract {\n /// @notice Init function that gets called from `BoringFactory.deploy`.\n /// Also kown as the constructor for cloned contracts.\n /// Any ETH send to `BoringFactory.deploy` ends up here.\n /// @param data Can be abi encoded arguments or anything else.\n function init(bytes calldata data) external payable;\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"./BoringMath.sol\";\n\nstruct Rebase {\n uint128 elastic;\n uint128 base;\n}\n\n/// @notice A rebasing library using overflow-/underflow-safe math.\nlibrary RebaseLibrary {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n\n /// @notice Calculates the base value in relationship to `elastic` and `total`.\n function toBase(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (uint256 base) {\n if (total.elastic == 0) {\n base = elastic;\n } else {\n base = elastic.mul(total.base) / total.elastic;\n if (roundUp && base.mul(total.elastic) / total.base < elastic) {\n base = base.add(1);\n }\n }\n }\n\n /// @notice Calculates the elastic value in relationship to `base` and `total`.\n function toElastic(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (uint256 elastic) {\n if (total.base == 0) {\n elastic = base;\n } else {\n elastic = base.mul(total.elastic) / total.base;\n if (roundUp && elastic.mul(total.base) / total.elastic < base) {\n elastic = elastic.add(1);\n }\n }\n }\n\n /// @notice Add `elastic` to `total` and doubles `total.base`.\n /// @return (Rebase) The new total.\n /// @return base in relationship to `elastic`.\n function add(\n Rebase memory total,\n uint256 elastic,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 base) {\n base = toBase(total, elastic, roundUp);\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return (total, base);\n }\n\n /// @notice Sub `base` from `total` and update `total.elastic`.\n /// @return (Rebase) The new total.\n /// @return elastic in relationship to `base`.\n function sub(\n Rebase memory total,\n uint256 base,\n bool roundUp\n ) internal pure returns (Rebase memory, uint256 elastic) {\n elastic = toElastic(total, base, roundUp);\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return (total, elastic);\n }\n\n /// @notice Add `elastic` and `base` to `total`.\n function add(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.add(elastic.to128());\n total.base = total.base.add(base.to128());\n return total;\n }\n\n /// @notice Subtract `elastic` and `base` to `total`.\n function sub(\n Rebase memory total,\n uint256 elastic,\n uint256 base\n ) internal pure returns (Rebase memory) {\n total.elastic = total.elastic.sub(elastic.to128());\n total.base = total.base.sub(base.to128());\n return total;\n }\n\n /// @notice Add `elastic` to `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function addElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.add(elastic.to128());\n }\n\n /// @notice Subtract `elastic` from `total` and update storage.\n /// @return newElastic Returns updated `elastic`.\n function subElastic(Rebase storage total, uint256 elastic) internal returns (uint256 newElastic) {\n newElastic = total.elastic = total.elastic.sub(elastic.to128());\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"../interfaces/IERC20.sol\";\n\n// solhint-disable avoid-low-level-calls\n\nlibrary BoringERC20 {\n bytes4 private constant SIG_SYMBOL = 0x95d89b41; // symbol()\n bytes4 private constant SIG_NAME = 0x06fdde03; // name()\n bytes4 private constant SIG_DECIMALS = 0x313ce567; // decimals()\n bytes4 private constant SIG_BALANCE_OF = 0x70a08231; // balanceOf(address)\n bytes4 private constant SIG_TRANSFER = 0xa9059cbb; // transfer(address,uint256)\n bytes4 private constant SIG_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256)\n\n function returnDataToString(bytes memory data) internal pure returns (string memory) {\n if (data.length >= 64) {\n return abi.decode(data, (string));\n } else if (data.length == 32) {\n uint8 i = 0;\n while (i < 32 && data[i] != 0) {\n i++;\n }\n bytes memory bytesArray = new bytes(i);\n for (i = 0; i < 32 && data[i] != 0; i++) {\n bytesArray[i] = data[i];\n }\n return string(bytesArray);\n } else {\n return \"???\";\n }\n }\n\n /// @notice Provides a safe ERC20.symbol version which returns '???' as fallback string.\n /// @param token The address of the ERC-20 token contract.\n /// @return (string) Token symbol.\n function safeSymbol(IERC20 token) internal view returns (string memory) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_SYMBOL));\n return success ? returnDataToString(data) : \"???\";\n }\n\n /// @notice Provides a safe ERC20.name version which returns '???' as fallback string.\n /// @param token The address of the ERC-20 token contract.\n /// @return (string) Token name.\n function safeName(IERC20 token) internal view returns (string memory) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_NAME));\n return success ? returnDataToString(data) : \"???\";\n }\n\n /// @notice Provides a safe ERC20.decimals version which returns '18' as fallback value.\n /// @param token The address of the ERC-20 token contract.\n /// @return (uint8) Token decimals.\n function safeDecimals(IERC20 token) internal view returns (uint8) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_DECIMALS));\n return success && data.length == 32 ? abi.decode(data, (uint8)) : 18;\n }\n\n /// @notice Provides a gas-optimized balance check to avoid a redundant extcodesize check in addition to the returndatasize check.\n /// @param token The address of the ERC-20 token.\n /// @param to The address of the user to check.\n /// @return amount The token amount.\n function safeBalanceOf(IERC20 token, address to) internal view returns (uint256 amount) {\n (bool success, bytes memory data) = address(token).staticcall(abi.encodeWithSelector(SIG_BALANCE_OF, to));\n require(success && data.length >= 32, \"BoringERC20: BalanceOf failed\");\n amount = abi.decode(data, (uint256));\n }\n\n /// @notice Provides a safe ERC20.transfer version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransfer(\n IERC20 token,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: Transfer failed\");\n }\n\n /// @notice Provides a safe ERC20.transferFrom version for different ERC-20 implementations.\n /// Reverts on a failed transfer.\n /// @param token The address of the ERC-20 token.\n /// @param from Transfer tokens from.\n /// @param to Transfer tokens to.\n /// @param amount The token amount.\n function safeTransferFrom(\n IERC20 token,\n address from,\n address to,\n uint256 amount\n ) internal {\n (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(SIG_TRANSFER_FROM, from, to, amount));\n require(success && (data.length == 0 || abi.decode(data, (bool))), \"BoringERC20: TransferFrom failed\");\n }\n}\n" + }, + "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\nimport '@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol';\nimport './IBatchFlashBorrower.sol';\nimport './IFlashBorrower.sol';\nimport './IStrategy.sol';\n\ninterface IBentoBoxV1 {\n event LogDeploy(address indexed masterContract, bytes data, address indexed cloneAddress);\n event LogDeposit(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event LogFlashLoan(address indexed borrower, address indexed token, uint256 amount, uint256 feeAmount, address indexed receiver);\n event LogRegisterProtocol(address indexed protocol);\n event LogSetMasterContractApproval(address indexed masterContract, address indexed user, bool approved);\n event LogStrategyDivest(address indexed token, uint256 amount);\n event LogStrategyInvest(address indexed token, uint256 amount);\n event LogStrategyLoss(address indexed token, uint256 amount);\n event LogStrategyProfit(address indexed token, uint256 amount);\n event LogStrategyQueued(address indexed token, address indexed strategy);\n event LogStrategySet(address indexed token, address indexed strategy);\n event LogStrategyTargetPercentage(address indexed token, uint256 targetPercentage);\n event LogTransfer(address indexed token, address indexed from, address indexed to, uint256 share);\n event LogWhiteListMasterContract(address indexed masterContract, bool approved);\n event LogWithdraw(address indexed token, address indexed from, address indexed to, uint256 amount, uint256 share);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n function balanceOf(IERC20, address) external view returns (uint256);\n function batch(bytes[] calldata calls, bool revertOnFail) external payable returns (bool[] memory successes, bytes[] memory results);\n function batchFlashLoan(IBatchFlashBorrower borrower, address[] calldata receivers, IERC20[] calldata tokens, uint256[] calldata amounts, bytes calldata data) external;\n function claimOwnership() external;\n function deploy(address masterContract, bytes calldata data, bool useCreate2) external payable;\n function deposit(IERC20 token_, address from, address to, uint256 amount, uint256 share) external payable returns (uint256 amountOut, uint256 shareOut);\n function flashLoan(IFlashBorrower borrower, address receiver, IERC20 token, uint256 amount, bytes calldata data) external;\n function harvest(IERC20 token, bool balance, uint256 maxChangeAmount) external;\n function masterContractApproved(address, address) external view returns (bool);\n function masterContractOf(address) external view returns (address);\n function nonces(address) external view returns (uint256);\n function owner() external view returns (address);\n function pendingOwner() external view returns (address);\n function pendingStrategy(IERC20) external view returns (IStrategy);\n function permitToken(IERC20 token, address from, address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;\n function registerProtocol() external;\n function setMasterContractApproval(address user, address masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) external;\n function setStrategy(IERC20 token, IStrategy newStrategy) external;\n function setStrategyTargetPercentage(IERC20 token, uint64 targetPercentage_) external;\n function strategy(IERC20) external view returns (IStrategy);\n function strategyData(IERC20) external view returns (uint64 strategyStartDate, uint64 targetPercentage, uint128 balance);\n function toAmount(IERC20 token, uint256 share, bool roundUp) external view returns (uint256 amount);\n function toShare(IERC20 token, uint256 amount, bool roundUp) external view returns (uint256 share);\n function totals(IERC20) external view returns (Rebase memory totals_);\n function transfer(IERC20 token, address from, address to, uint256 share) external;\n function transferMultiple(IERC20 token, address from, address[] calldata tos, uint256[] calldata shares) external;\n function transferOwnership(address newOwner, bool direct, bool renounce) external;\n function whitelistMasterContract(address masterContract, bool approved) external;\n function whitelistedMasterContracts(address) external view returns (bool);\n function withdraw(IERC20 token_, address from, address to, uint256 amount, uint256 share) external returns (uint256 amountOut, uint256 shareOut);\n}" + }, + "contracts/MagicInternetMoney.sol": { + "content": "// SPDX-License-Identifier: MIT\r\n\r\n// Magic Internet Money\r\n\r\n// ███╗ ███╗██╗███╗ ███╗\r\n// ████╗ ████║██║████╗ ████║\r\n// ██╔████╔██║██║██╔████╔██║\r\n// ██║╚██╔╝██║██║██║╚██╔╝██║\r\n// ██║ ╚═╝ ██║██║██║ ╚═╝ ██║\r\n// ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝\r\n\r\n// BoringCrypto, 0xMerlin\r\n\r\npragma solidity 0.6.12;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract MagicInternetMoney is ERC20, BoringOwnable {\r\n using BoringMath for uint256;\r\n // ERC20 'variables'\r\n string public constant symbol = \"MIM\";\r\n string public constant name = \"Magic Internet Money\";\r\n uint8 public constant decimals = 18;\r\n uint256 public override totalSupply;\r\n\r\n struct Minting {\r\n uint128 time;\r\n uint128 amount;\r\n }\r\n\r\n Minting public lastMint;\r\n uint256 private constant MINTING_PERIOD = 24 hours;\r\n uint256 private constant MINTING_INCREASE = 15000;\r\n uint256 private constant MINTING_PRECISION = 1e5;\r\n\r\n function mint(address to, uint256 amount) public onlyOwner {\r\n require(to != address(0), \"MIM: no mint to zero address\");\r\n\r\n // Limits the amount minted per period to a convergence function, with the period duration restarting on every mint\r\n uint256 totalMintedAmount = uint256(lastMint.time < block.timestamp - MINTING_PERIOD ? 0 : lastMint.amount).add(amount);\r\n require(totalSupply == 0 || totalSupply.mul(MINTING_INCREASE) / MINTING_PRECISION >= totalMintedAmount);\r\n\r\n lastMint.time = block.timestamp.to128();\r\n lastMint.amount = totalMintedAmount.to128();\r\n\r\n totalSupply = totalSupply + amount;\r\n balanceOf[to] += amount;\r\n emit Transfer(address(0), to, amount);\r\n }\r\n\r\n function mintToBentoBox(address clone, uint256 amount, IBentoBoxV1 bentoBox) public onlyOwner {\r\n mint(address(bentoBox), amount);\r\n bentoBox.deposit(IERC20(address(this)), address(bentoBox), clone, amount, 0);\r\n }\r\n\r\n function burn(uint256 amount) public {\r\n require(amount <= balanceOf[msg.sender], \"MIM: not enough\");\r\n\r\n balanceOf[msg.sender] -= amount;\r\n totalSupply -= amount;\r\n emit Transfer(msg.sender, address(0), amount);\r\n }\r\n}\r\n" + }, + "contracts/interfaces/IOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\n\ninterface IOracle {\n /// @notice Get the latest exchange rate.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return success if no valid (recent) rate is available, return false else true.\n /// @return rate The rate of the requested asset / pair / pool.\n function get(bytes calldata data) external returns (bool success, uint256 rate);\n\n /// @notice Check the last exchange rate without any state changes.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return success if no valid (recent) rate is available, return false else true.\n /// @return rate The rate of the requested asset / pair / pool.\n function peek(bytes calldata data) external view returns (bool success, uint256 rate);\n\n /// @notice Check the current spot exchange rate without any state changes. For oracles like TWAP this will be different from peek().\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return rate The rate of the requested asset / pair / pool.\n function peekSpot(bytes calldata data) external view returns (uint256 rate);\n\n /// @notice Returns a human readable (short) name about this oracle.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return (string) A human readable symbol name about this oracle.\n function symbol(bytes calldata data) external view returns (string memory);\n\n /// @notice Returns a human readable name about this oracle.\n /// @param data Usually abi encoded, implementation specific data that contains information and arguments to & about the oracle.\n /// For example:\n /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n /// @return (string) A human readable name about this oracle.\n function name(bytes calldata data) external view returns (string memory);\n}\n" + }, + "contracts/interfaces/ISwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\n\ninterface ISwapper {\n /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for at least 'amountToMin' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Returns the amount of tokens 'to' transferred to BentoBox.\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) external returns (uint256 extraShare, uint256 shareReturned);\n\n /// @notice Calculates the amount of token 'from' needed to complete the swap (amountFrom),\n /// this should be less than or equal to amountFromMax.\n /// Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for exactly 'exactAmountTo' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Transfers allocated, but unused 'from' tokens within the BentoBox to 'refundTo' (amountFromMax - amountFrom).\n /// Returns the amount of 'from' tokens withdrawn from BentoBox (amountFrom).\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) external returns (uint256 shareUsed, uint256 shareReturned);\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IERC20 {\n function totalSupply() external view returns (uint256);\n\n function balanceOf(address account) external view returns (uint256);\n\n function allowance(address owner, address spender) external view returns (uint256);\n\n function approve(address spender, uint256 amount) external returns (bool);\n\n event Transfer(address indexed from, address indexed to, uint256 value);\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /// @notice EIP 2612\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/Domain.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Based on code and smartness by Ross Campbell and Keno\n// Uses immutable to store the domain separator to reduce gas usage\n// If the chain id changes due to a fork, the forked chain will calculate on the fly.\npragma solidity 0.6.12;\n\n// solhint-disable no-inline-assembly\n\ncontract Domain {\n bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\"EIP712Domain(uint256 chainId,address verifyingContract)\");\n // See https://eips.ethereum.org/EIPS/eip-191\n string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = \"\\x19\\x01\";\n\n // solhint-disable var-name-mixedcase\n bytes32 private immutable _DOMAIN_SEPARATOR;\n uint256 private immutable DOMAIN_SEPARATOR_CHAIN_ID;\n\n /// @dev Calculate the DOMAIN_SEPARATOR\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\n return keccak256(abi.encode(DOMAIN_SEPARATOR_SIGNATURE_HASH, chainId, address(this)));\n }\n\n constructor() public {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(DOMAIN_SEPARATOR_CHAIN_ID = chainId);\n }\n\n /// @dev Return the DOMAIN_SEPARATOR\n // It's named internal to allow making it public from the contract that uses it by creating a simple view function\n // with the desired public name, such as DOMAIN_SEPARATOR or domainSeparator.\n // solhint-disable-next-line func-name-mixedcase\n function _domainSeparator() internal view returns (bytes32) {\n uint256 chainId;\n assembly {\n chainId := chainid()\n }\n return chainId == DOMAIN_SEPARATOR_CHAIN_ID ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(chainId);\n }\n\n function _getDigest(bytes32 dataHash) internal view returns (bytes32 digest) {\n digest = keccak256(abi.encodePacked(EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, _domainSeparator(), dataHash));\n }\n}\n" + }, + "@sushiswap/bentobox-sdk/contracts/IBatchFlashBorrower.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\n\ninterface IBatchFlashBorrower {\n function onBatchFlashLoan(\n address sender,\n IERC20[] calldata tokens,\n uint256[] calldata amounts,\n uint256[] calldata fees,\n bytes calldata data\n ) external;\n}" + }, + "@sushiswap/bentobox-sdk/contracts/IFlashBorrower.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport '@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol';\n\ninterface IFlashBorrower {\n function onFlashLoan(\n address sender,\n IERC20 token,\n uint256 amount,\n uint256 fee,\n bytes calldata data\n ) external;\n}" + }, + "@sushiswap/bentobox-sdk/contracts/IStrategy.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IStrategy {\n // Send the assets to the Strategy and call skim to invest them\n function skim(uint256 amount) external;\n\n // Harvest any profits made converted to the asset and pass them to the caller\n function harvest(uint256 balance, address sender) external returns (int256 amountAdded);\n\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding.\n // The actualAmount should be very close to the amount. The difference should NOT be used to report a loss. That's what harvest is for.\n function withdraw(uint256 amount) external returns (uint256 actualAmount);\n\n // Withdraw all assets in the safest way possible. This shouldn't fail.\n function exit(uint256 balance) external returns (int256 amountAdded);\n}" + }, + "contracts/swappers/SushiSwapSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ncontract SushiSwapSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n IUniswapV2Factory public immutable factory;\n bytes32 public immutable pairCodeHash;\n\n constructor(\n IBentoBoxV1 bentoBox_,\n IUniswapV2Factory factory_,\n bytes32 pairCodeHash_\n ) public {\n bentoBox = bentoBox_;\n factory = factory_;\n pairCodeHash = pairCodeHash_;\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n (IERC20 token0, IERC20 token1) = fromToken < toToken ? (fromToken, toToken) : (toToken, fromToken);\n IUniswapV2Pair pair =\n IUniswapV2Pair(\n uint256(\n keccak256(abi.encodePacked(hex\"ff\", factory, keccak256(abi.encodePacked(address(token0), address(token1))), pairCodeHash))\n )\n );\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(pair), 0, shareFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n uint256 amountTo;\n if (toToken > fromToken) {\n amountTo = getAmountOut(amountFrom, reserve0, reserve1);\n pair.swap(0, amountTo, address(bentoBox), new bytes(0));\n } else {\n amountTo = getAmountOut(amountFrom, reserve1, reserve0);\n pair.swap(amountTo, 0, address(bentoBox), new bytes(0));\n }\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n IUniswapV2Pair pair;\n {\n (IERC20 token0, IERC20 token1) = fromToken < toToken ? (fromToken, toToken) : (toToken, fromToken);\n pair = IUniswapV2Pair(\n uint256(\n keccak256(abi.encodePacked(hex\"ff\", factory, keccak256(abi.encodePacked(address(token0), address(token1))), pairCodeHash))\n )\n );\n }\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n\n uint256 amountToExact = bentoBox.toAmount(toToken, shareToExact, true);\n\n uint256 amountFrom;\n if (toToken > fromToken) {\n amountFrom = getAmountIn(amountToExact, reserve0, reserve1);\n (, shareUsed) = bentoBox.withdraw(fromToken, address(this), address(pair), amountFrom, 0);\n pair.swap(0, amountToExact, address(bentoBox), \"\");\n } else {\n amountFrom = getAmountIn(amountToExact, reserve1, reserve0);\n (, shareUsed) = bentoBox.withdraw(fromToken, address(this), address(pair), amountFrom, 0);\n pair.swap(amountToExact, 0, address(bentoBox), \"\");\n }\n bentoBox.deposit(toToken, address(bentoBox), recipient, 0, shareToExact);\n shareReturned = shareFromSupplied.sub(shareUsed);\n if (shareReturned > 0) {\n bentoBox.transfer(fromToken, address(this), refundTo, shareReturned);\n }\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.5.0;\n\ninterface IUniswapV2Factory {\n event PairCreated(address indexed token0, address indexed token1, address pair, uint);\n\n function feeTo() external view returns (address);\n function feeToSetter() external view returns (address);\n function migrator() external view returns (address);\n\n function getPair(address tokenA, address tokenB) external view returns (address pair);\n function allPairs(uint) external view returns (address pair);\n function allPairsLength() external view returns (uint);\n\n function createPair(address tokenA, address tokenB) external returns (address pair);\n\n function setFeeTo(address) external;\n function setFeeToSetter(address) external;\n function setMigrator(address) external;\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.5.0;\n\ninterface IUniswapV2Pair {\n event Approval(address indexed owner, address indexed spender, uint value);\n event Transfer(address indexed from, address indexed to, uint value);\n\n function name() external pure returns (string memory);\n function symbol() external pure returns (string memory);\n function decimals() external pure returns (uint8);\n function totalSupply() external view returns (uint);\n function balanceOf(address owner) external view returns (uint);\n function allowance(address owner, address spender) external view returns (uint);\n\n function approve(address spender, uint value) external returns (bool);\n function transfer(address to, uint value) external returns (bool);\n function transferFrom(address from, address to, uint value) external returns (bool);\n\n function DOMAIN_SEPARATOR() external view returns (bytes32);\n function PERMIT_TYPEHASH() external pure returns (bytes32);\n function nonces(address owner) external view returns (uint);\n\n function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;\n\n event Mint(address indexed sender, uint amount0, uint amount1);\n event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);\n event Swap(\n address indexed sender,\n uint amount0In,\n uint amount1In,\n uint amount0Out,\n uint amount1Out,\n address indexed to\n );\n event Sync(uint112 reserve0, uint112 reserve1);\n\n function MINIMUM_LIQUIDITY() external pure returns (uint);\n function factory() external view returns (address);\n function token0() external view returns (address);\n function token1() external view returns (address);\n function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);\n function price0CumulativeLast() external view returns (uint);\n function price1CumulativeLast() external view returns (uint);\n function kLast() external view returns (uint);\n\n function mint(address to) external returns (uint liquidity);\n function burn(address to) external returns (uint amount0, uint amount1);\n function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;\n function skim(address to) external;\n function sync() external;\n\n function initialize(address, address) external;\n}" + }, + "contracts/swappers/SushiSwapMultiSwapper.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../libraries/UniswapV2Library.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/libraries/TransferHelper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ncontract SushiSwapMultiSwapper {\n using BoringERC20 for IERC20;\n using BoringMath for uint256;\n\n address private immutable factory;\n\n IBentoBoxV1 private immutable bentoBox;\n\n bytes32 private immutable pairCodeHash;\n\n constructor(\n address _factory,\n IBentoBoxV1 _bentoBox,\n bytes32 _pairCodeHash\n ) public {\n factory = _factory;\n bentoBox = _bentoBox;\n pairCodeHash = _pairCodeHash;\n }\n\n function getOutputAmount(\n IERC20 tokenIn,\n IERC20 tokenOut,\n uint256 amountMinOut,\n address[] calldata path,\n uint256 shareIn\n ) external view returns (uint256 amountOut) {\n uint256 amountIn = bentoBox.toAmount(tokenIn, shareIn, false);\n uint256[] memory amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path, pairCodeHash);\n amountOut = amounts[amounts.length - 1];\n }\n\n function swap(\n IERC20 tokenIn,\n IERC20 tokenOut,\n uint256 amountMinOut,\n address[] calldata path,\n uint256 shareIn\n ) external returns (uint256 amountOut, uint256 shareOut) {\n (uint256 amountIn, ) = bentoBox.withdraw(tokenIn, address(this), address(this), 0, shareIn);\n amountOut = _swapExactTokensForTokens(amountIn, amountMinOut, path, address(bentoBox));\n (, shareOut) = bentoBox.deposit(tokenOut, address(bentoBox), msg.sender, amountOut, 0);\n }\n\n // Swaps an exact amount of tokens for another token through the path passed as an argument\n // Returns the amount of the final token\n function _swapExactTokensForTokens(\n uint256 amountIn,\n uint256 amountOutMin,\n address[] memory path,\n address to\n ) internal returns (uint256 amountOut) {\n uint256[] memory amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path, pairCodeHash);\n amountOut = amounts[amounts.length - 1];\n require(amountOut >= amountOutMin, \"insufficient-amount-out\");\n IERC20(path[0]).safeTransfer(UniswapV2Library.pairFor(factory, path[0], path[1], pairCodeHash), amountIn);\n _swap(amounts, path, to);\n }\n\n // requires the initial amount to have already been sent to the first pair\n function _swap(\n uint256[] memory amounts,\n address[] memory path,\n address _to\n ) internal virtual {\n for (uint256 i; i < path.length - 1; i++) {\n (address input, address output) = (path[i], path[i + 1]);\n (address token0, ) = UniswapV2Library.sortTokens(input, output);\n uint256 amountOut = amounts[i + 1];\n (uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));\n address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2], pairCodeHash) : _to;\n IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output, pairCodeHash)).swap(amount0Out, amount1Out, to, new bytes(0));\n }\n }\n}\n" + }, + "contracts/libraries/UniswapV2Library.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.5.0;\n\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\n\nimport \"@sushiswap/core/contracts/uniswapv2/libraries/SafeMath.sol\";\n\nlibrary UniswapV2Library {\n using SafeMathUniswap for uint256;\n\n // returns sorted token addresses, used to handle return values from pairs sorted in this order\n function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {\n require(tokenA != tokenB, \"UniswapV2Library: IDENTICAL_ADDRESSES\");\n (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);\n require(token0 != address(0), \"UniswapV2Library: ZERO_ADDRESS\");\n }\n\n // calculates the CREATE2 address for a pair without making any external calls\n function pairFor(\n address factory,\n address tokenA,\n address tokenB,\n bytes32 pairCodeHash\n ) internal pure returns (address pair) {\n (address token0, address token1) = sortTokens(tokenA, tokenB);\n pair = address(\n uint256(\n keccak256(\n abi.encodePacked(\n hex\"ff\",\n factory,\n keccak256(abi.encodePacked(token0, token1)),\n pairCodeHash // init code hash\n )\n )\n )\n );\n }\n\n // fetches and sorts the reserves for a pair\n function getReserves(\n address factory,\n address tokenA,\n address tokenB,\n bytes32 pairCodeHash\n ) internal view returns (uint256 reserveA, uint256 reserveB) {\n (address token0, ) = sortTokens(tokenA, tokenB);\n (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB, pairCodeHash)).getReserves();\n (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);\n }\n\n // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset\n function quote(\n uint256 amountA,\n uint256 reserveA,\n uint256 reserveB\n ) internal pure returns (uint256 amountB) {\n require(amountA > 0, \"UniswapV2Library: INSUFFICIENT_AMOUNT\");\n require(reserveA > 0 && reserveB > 0, \"UniswapV2Library: INSUFFICIENT_LIQUIDITY\");\n amountB = amountA.mul(reserveB) / reserveA;\n }\n\n // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n require(amountIn > 0, \"UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT\");\n require(reserveIn > 0 && reserveOut > 0, \"UniswapV2Library: INSUFFICIENT_LIQUIDITY\");\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n require(amountOut > 0, \"UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT\");\n require(reserveIn > 0 && reserveOut > 0, \"UniswapV2Library: INSUFFICIENT_LIQUIDITY\");\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // performs chained getAmountOut calculations on any number of pairs\n function getAmountsOut(\n address factory,\n uint256 amountIn,\n address[] memory path,\n bytes32 pairCodeHash\n ) internal view returns (uint256[] memory amounts) {\n require(path.length >= 2, \"UniswapV2Library: INVALID_PATH\");\n amounts = new uint256[](path.length);\n amounts[0] = amountIn;\n for (uint256 i; i < path.length - 1; i++) {\n (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i], path[i + 1], pairCodeHash);\n amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);\n }\n }\n\n // performs chained getAmountIn calculations on any number of pairs\n function getAmountsIn(\n address factory,\n uint256 amountOut,\n address[] memory path,\n bytes32 pairCodeHash\n ) internal view returns (uint256[] memory amounts) {\n require(path.length >= 2, \"UniswapV2Library: INVALID_PATH\");\n amounts = new uint256[](path.length);\n amounts[amounts.length - 1] = amountOut;\n for (uint256 i = path.length - 1; i > 0; i--) {\n (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i], pairCodeHash);\n amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);\n }\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/libraries/TransferHelper.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.6.0;\n\n// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false\nlibrary TransferHelper {\n function safeApprove(address token, address to, uint value) internal {\n // bytes4(keccak256(bytes('approve(address,uint256)')));\n (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));\n require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED');\n }\n\n function safeTransfer(address token, address to, uint value) internal {\n // bytes4(keccak256(bytes('transfer(address,uint256)')));\n (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));\n require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');\n }\n\n function safeTransferFrom(address token, address from, address to, uint value) internal {\n // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));\n (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));\n require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');\n }\n\n function safeTransferETH(address to, uint value) internal {\n (bool success,) = to.call{value:value}(new bytes(0));\n require(success, 'TransferHelper: ETH_TRANSFER_FAILED');\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/libraries/SafeMath.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\n// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math)\n\nlibrary SafeMathUniswap {\n function add(uint x, uint y) internal pure returns (uint z) {\n require((z = x + y) >= x, 'ds-math-add-overflow');\n }\n\n function sub(uint x, uint y) internal pure returns (uint z) {\n require((z = x - y) <= x, 'ds-math-sub-underflow');\n }\n\n function mul(uint x, uint y) internal pure returns (uint z) {\n require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');\n }\n}\n" + }, + "contracts/swappers/SpellSwapper.sol": { + "content": "// License-Identifier: MIT\npragma solidity 0.6.12;\n\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../libraries/UniswapV2Library.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/libraries/TransferHelper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract SpellSwapper is BoringOwnable {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant SPELL = IERC20(0x090185f2135308BaD17527004364eBcC2D37e5F6);\n address public constant sSPELL = 0x26FA3fFFB6EfE8c1E69103aCb4044C26B9A106a9;\n IUniswapV2Pair constant SPELL_WETH = IUniswapV2Pair(0xb5De0C3753b6E1B4dBA616Db82767F17513E6d4E);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n address public constant treasury = 0x5A7C5505f3CFB9a0D9A8493EC41bf27EE48c406D;\n mapping (address => bool) public verified;\n\n constructor(\n ) public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n verified[msg.sender] = true;\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n modifier onlyVerified {\n require(verified[msg.sender], \"Only verified operators\");\n _;\n }\n\n function setVerified(address operator, bool status) public onlyOwner {\n verified[operator] = status;\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n uint256 amountToMin\n ) public onlyVerified{\n\n uint256 amountFirst;\n uint256 amountIntermediate;\n\n {\n\n uint256 shareFrom = bentoBox.balanceOf(MIM, address(this));\n\n uint256 treasuryShare = shareFrom / 4;\n\n bentoBox.withdraw(MIM, address(this), treasury, 0, treasuryShare);\n\n (uint256 amountMIMFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom.sub(treasuryShare));\n\n amountFirst = MIM3POOL.exchange_underlying(0, 3, amountMIMFrom, 0, address(pair));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n\n }\n\n uint256 amountThird;\n\n {\n \n pair.swap(amountIntermediate, 0, address(SPELL_WETH), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = SPELL_WETH.getReserves();\n \n amountThird = getAmountOut(amountIntermediate, reserve1, reserve0);\n \n require(amountThird >= amountToMin, \"Minimum must be reached\");\n\n }\n\n SPELL_WETH.swap(amountThird, 0, sSPELL, new bytes(0));\n }\n\n}" + }, + "contracts/Spell.sol": { + "content": "// SPDX-License-Identifier: MIT\n\n// Spell\n\n// Special thanks to:\n// @BoringCrypto for his great libraries\n\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\n/// @title Spell\n/// @author 0xMerlin\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract Spell is ERC20, BoringOwnable {\n using BoringMath for uint256;\n // ERC20 'variables'\n string public constant symbol = \"SPELL\";\n string public constant name = \"Spell Token\";\n uint8 public constant decimals = 18;\n uint256 public override totalSupply;\n uint256 public constant MAX_SUPPLY = 420 * 1e27;\n\n function mint(address to, uint256 amount) public onlyOwner {\n require(to != address(0), \"SPELL: no mint to zero address\");\n require(MAX_SUPPLY >= totalSupply.add(amount), \"SPELL: Don't go over MAX\");\n\n totalSupply = totalSupply + amount;\n balanceOf[to] += amount;\n emit Transfer(address(0), to, amount);\n }\n}\n" + }, + "contracts/sSpell.sol": { + "content": "//SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/Domain.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringBatchable.sol\";\n\n\n// Staking in sSpell inspired by Chef Nomi's SushiBar - MIT license (originally WTFPL)\n// modified by BoringCrypto for DictatorDAO\n\ncontract sSpell is IERC20, Domain {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using BoringERC20 for IERC20;\n\n string public constant symbol = \"sSPELL\";\n string public constant name = \"Staked Spell Tokens\";\n uint8 public constant decimals = 18;\n uint256 public override totalSupply;\n uint256 private constant LOCK_TIME = 24 hours;\n\n IERC20 public immutable token;\n\n constructor(IERC20 _token) public {\n token = _token;\n }\n\n struct User {\n uint128 balance;\n uint128 lockedUntil;\n }\n\n /// @notice owner > balance mapping.\n mapping(address => User) public users;\n /// @notice owner > spender > allowance mapping.\n mapping(address => mapping(address => uint256)) public override allowance;\n /// @notice owner > nonce mapping. Used in `permit`.\n mapping(address => uint256) public nonces;\n\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n\n function balanceOf(address user) public view override returns (uint256 balance) {\n return users[user].balance;\n }\n\n function _transfer(\n address from,\n address to,\n uint256 shares\n ) internal {\n User memory fromUser = users[from];\n require(block.timestamp >= fromUser.lockedUntil, \"Locked\");\n if (shares != 0) {\n require(fromUser.balance >= shares, \"Low balance\");\n if (from != to) {\n require(to != address(0), \"Zero address\"); // Moved down so other failed calls safe some gas\n User memory toUser = users[to];\n users[from].balance = fromUser.balance - shares.to128(); // Underflow is checked\n users[to].balance = toUser.balance + shares.to128(); // Can't overflow because totalSupply would be greater than 2^128-1;\n }\n }\n emit Transfer(from, to, shares);\n }\n\n function _useAllowance(address from, uint256 shares) internal {\n if (msg.sender == from) {\n return;\n }\n uint256 spenderAllowance = allowance[from][msg.sender];\n // If allowance is infinite, don't decrease it to save on gas (breaks with EIP-20).\n if (spenderAllowance != type(uint256).max) {\n require(spenderAllowance >= shares, \"Low allowance\");\n allowance[from][msg.sender] = spenderAllowance - shares; // Underflow is checked\n }\n }\n\n /// @notice Transfers `shares` tokens from `msg.sender` to `to`.\n /// @param to The address to move the tokens.\n /// @param shares of the tokens to move.\n /// @return (bool) Returns True if succeeded.\n function transfer(address to, uint256 shares) public returns (bool) {\n _transfer(msg.sender, to, shares);\n return true;\n }\n\n /// @notice Transfers `shares` tokens from `from` to `to`. Caller needs approval for `from`.\n /// @param from Address to draw tokens from.\n /// @param to The address to move the tokens.\n /// @param shares The token shares to move.\n /// @return (bool) Returns True if succeeded.\n function transferFrom(\n address from,\n address to,\n uint256 shares\n ) public returns (bool) {\n _useAllowance(from, shares);\n _transfer(from, to, shares);\n return true;\n }\n\n /// @notice Approves `amount` from sender to be spend by `spender`.\n /// @param spender Address of the party that can draw from msg.sender's account.\n /// @param amount The maximum collective amount that `spender` can draw.\n /// @return (bool) Returns True if approved.\n function approve(address spender, uint256 amount) public override returns (bool) {\n allowance[msg.sender][spender] = amount;\n emit Approval(msg.sender, spender, amount);\n return true;\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n bytes32 private constant PERMIT_SIGNATURE_HASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\n\n /// @notice Approves `value` from `owner_` to be spend by `spender`.\n /// @param owner_ Address of the owner.\n /// @param spender The address of the spender that gets approved to draw from `owner_`.\n /// @param value The maximum collective amount that `spender` can draw.\n /// @param deadline This permit must be redeemed before this deadline (UTC timestamp in seconds).\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external override {\n require(owner_ != address(0), \"Zero owner\");\n require(block.timestamp < deadline, \"Expired\");\n require(\n ecrecover(_getDigest(keccak256(abi.encode(PERMIT_SIGNATURE_HASH, owner_, spender, value, nonces[owner_]++, deadline))), v, r, s) ==\n owner_,\n \"Invalid Sig\"\n );\n allowance[owner_][spender] = value;\n emit Approval(owner_, spender, value);\n }\n\n /// math is ok, because amount, totalSupply and shares is always 0 <= amount <= 100.000.000 * 10^18\n /// theoretically you can grow the amount/share ratio, but it's not practical and useless\n function mint(uint256 amount) public returns (bool) {\n require(msg.sender != address(0), \"Zero address\");\n User memory user = users[msg.sender];\n\n uint256 totalTokens = token.balanceOf(address(this));\n uint256 shares = totalSupply == 0 ? amount : (amount * totalSupply) / totalTokens;\n user.balance += shares.to128();\n user.lockedUntil = (block.timestamp + LOCK_TIME).to128();\n users[msg.sender] = user;\n totalSupply += shares;\n\n token.safeTransferFrom(msg.sender, address(this), amount);\n\n emit Transfer(address(0), msg.sender, shares);\n return true;\n }\n\n function _burn(\n address from,\n address to,\n uint256 shares\n ) internal {\n require(to != address(0), \"Zero address\");\n User memory user = users[from];\n require(block.timestamp >= user.lockedUntil, \"Locked\");\n uint256 amount = (shares * token.balanceOf(address(this))) / totalSupply;\n users[from].balance = user.balance.sub(shares.to128()); // Must check underflow\n totalSupply -= shares;\n\n token.safeTransfer(to, amount);\n\n emit Transfer(from, address(0), shares);\n }\n\n function burn(address to, uint256 shares) public returns (bool) {\n _burn(msg.sender, to, shares);\n return true;\n }\n\n function burnFrom(\n address from,\n address to,\n uint256 shares\n ) public returns (bool) {\n _useAllowance(from, shares);\n _burn(from, to, shares);\n return true;\n }\n}" + }, + "@boringcrypto/boring-solidity/contracts/BoringBatchable.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\n\n// solhint-disable avoid-low-level-calls\n// solhint-disable no-inline-assembly\n\n// Audit on 5-Jan-2021 by Keno and BoringCrypto\n// WARNING!!!\n// Combining BoringBatchable with msg.value can cause double spending issues\n// https://www.paradigm.xyz/2021/08/two-rights-might-make-a-wrong/\n\nimport \"./interfaces/IERC20.sol\";\n\ncontract BaseBoringBatchable {\n /// @dev Helper function to extract a useful revert message from a failed call.\n /// If the returned data is malformed or not correctly abi encoded then this call can fail itself.\n function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {\n // If the _res length is less than 68, then the transaction failed silently (without a revert message)\n if (_returnData.length < 68) return \"Transaction reverted silently\";\n\n assembly {\n // Slice the sighash.\n _returnData := add(_returnData, 0x04)\n }\n return abi.decode(_returnData, (string)); // All that remains is the revert string\n }\n\n /// @notice Allows batched call to self (this contract).\n /// @param calls An array of inputs for each call.\n /// @param revertOnFail If True then reverts after a failed call and stops doing further calls.\n // F1: External is ok here because this is the batch function, adding it to a batch makes no sense\n // F2: Calls in the batch may be payable, delegatecall operates in the same context, so each call in the batch has access to msg.value\n // C3: The length of the loop is fully under user control, so can't be exploited\n // C7: Delegatecall is only used on the same contract, so it's safe\n function batch(bytes[] calldata calls, bool revertOnFail) external payable {\n for (uint256 i = 0; i < calls.length; i++) {\n (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\n if (!success && revertOnFail) {\n revert(_getRevertMsg(result));\n }\n }\n }\n}\n\ncontract BoringBatchable is BaseBoringBatchable {\n /// @notice Call wrapper that performs `ERC20.permit` on `token`.\n /// Lookup `IERC20.permit`.\n // F6: Parameters can be used front-run the permit and the user's permit will fail (due to nonce or other revert)\n // if part of a batch this could be used to grief once as the second call would not need the permit\n function permitToken(\n IERC20 token,\n address from,\n address to,\n uint256 amount,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n token.permit(from, to, amount, deadline, v, r, s);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVYFISwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw(uint256 maxShares, address recipient) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVYFISwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n YearnVault public constant YFI_VAULT = YearnVault(0xE14d13d8B3b85aF791b2AADD661cDBd5E6097Db1);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7);\n IUniswapV2Pair constant YFI_WETH = IUniswapV2Pair(0x088ee5007C98a9677165D78dD2109AE4a3D04d0C);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n \n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n uint256 amountFirst;\n\n {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n uint256 amountFrom = YFI_VAULT.withdraw(type(uint256).max, address(YFI_WETH));\n\n (uint256 reserve0, uint256 reserve1, ) = YFI_WETH.getReserves();\n \n amountFirst = getAmountOut(amountFrom, reserve0, reserve1);\n\n }\n \n YFI_WETH.swap(0, amountFirst, address(pair), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFirst, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVWETHSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw(uint256 maxShares, address recipient) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\ncontract YVWETHSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n YearnVault public constant WETH_VAULT = YearnVault(0xa258C4606Ca8206D8aA700cE2143D7db854D168c);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n uint256 amountFrom = WETH_VAULT.withdraw(type(uint256).max, address(pair));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFrom, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVUSDTSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVUSDTSwapper is ISwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n YearnVault public constant TETHER_VAULT = YearnVault(0x7Da96a3891Add058AdA2E826306D812C638D87a7);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n uint256 amountFrom =TETHER_VAULT.withdraw();\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountFrom, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (1,1);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVUSDCSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n}\n\ncontract YVUSDCSwapper is ISwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IERC20 public constant TETHER = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n YearnVault public constant USDC_VAULT = YearnVault(0x5f18C75AbDAe578b483E5F43f12a39cF75b973a9);\n IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n USDC.approve(address(MIM3POOL), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n uint256 amountFrom = USDC_VAULT.withdraw();\n\n uint256 amountTo = MIM3POOL.exchange_underlying(2, 0, amountFrom, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (1,1);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVUSDTLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVUSDTLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n YearnVault public constant USDT_VAULT = YearnVault(0x7Da96a3891Add058AdA2E826306D812C638D87a7);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(USDT_VAULT), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n uint256 amountTo = USDT_VAULT.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(USDT_VAULT)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVUSDCLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\n\ncontract YVUSDCLeverageSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n YearnVault public constant USDC_VAULT = YearnVault(0x5f18C75AbDAe578b483E5F43f12a39cF75b973a9);\n IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n MIM.approve(address(MIM3POOL), type(uint256).max);\n USDC.approve(address(USDC_VAULT), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountIntermediate = MIM3POOL.exchange_underlying(0, 2, amountFrom, 0, address(this));\n\n uint256 amountTo = USDC_VAULT.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(USDC_VAULT)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVIBLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[3] memory amounts, uint256 _min_mint_amount, bool _use_underlying) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVIBLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public IronBank = CurvePool(0x2dded6Da1BF5DBdF597C45fcFaa3194e53EcfeAF);\n YearnVault constant public YVIB = YearnVault(0x27b7b1ad7288079A66d12350c828D3C00A6F07d7);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant CurveToken = IERC20(0x5282a4eF67D9C33135340fB3289cc1711c13638C);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(IronBank), type(uint256).max);\n CurveToken.approve(address(YVIB), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountIntermediate = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n uint256[3] memory amountsAdded = [0,0, amountIntermediate];\n\n IronBank.add_liquidity(amountsAdded, 0, true);\n\n uint256 amountTo = YVIB.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(YVIB)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVCrvStETHLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[3] memory amounts, uint256 _min_mint_amount, bool _use_underlying) external returns (uint256);\n function add_liquidity(uint256[2] memory amounts, uint256 _min_mint_amount) external payable returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\ninterface IWETH is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n function deposit() external payable;\n function withdraw(uint wad) external;\n}\n\ncontract YVCrvStETHLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public STETH = CurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);\n YearnVault constant public YVSTETH = YearnVault(0xdCD90C7f6324cfa40d7169ef80b12031770B4325);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant CurveToken = IERC20(0x06325440D014e39736583c165C2963BA99fAf14E);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IWETH public constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(STETH), type(uint256).max);\n CurveToken.approve(address(YVSTETH), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n receive() external payable {}\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n uint256 amountThird;\n {\n uint256 amountSecond = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(pair));\n \n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n amountThird = getAmountOut(amountSecond, reserve1, reserve0);\n pair.swap(amountThird, 0, address(this), new bytes(0));\n }\n\n WETH.withdraw(amountThird);\n \n uint256[2] memory amountsAdded = [amountThird,0];\n\n STETH.add_liquidity{value: amountThird}(amountsAdded, 0);\n\n uint256 amountTo = YVSTETH.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(YVSTETH)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/WethLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[2] memory amounts, uint256 _min_mint_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract WethLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant degenBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(threecrypto), type(uint256).max);\n WETH.approve(address(degenBox), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = degenBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountOne = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n threecrypto.exchange(0, 2, amountOne, 0);\n\n uint256 amountTo = WETH.balanceOf(address(this));\n\n (, shareReturned) = degenBox.deposit(WETH, address(this), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}" + }, + "contracts/swappers/Leverage/WbtcLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[2] memory amounts, uint256 _min_mint_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract WbtcLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant degenBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(threecrypto), type(uint256).max);\n WBTC.approve(address(degenBox), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = degenBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountOne = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n threecrypto.exchange(0, 1, amountOne, 0);\n\n uint256 amountTo = WBTC.balanceOf(address(this));\n\n (, shareReturned) = degenBox.deposit(WBTC, address(this), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}" + }, + "contracts/swappers/Leverage/ThreeCryptoLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[3] memory amounts, uint256 _min_mint_amount) external;\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n //deposit a curve token\n function deposit(uint256 _amount, address _to) external;\n}\n\ncontract ThreeCryptoLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public threecrypto = CurvePool(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n IConvex public constant cvx3Crypto = IConvex(0x5958A8DB7dfE0CC49382209069b00F54e17929C2);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant CurveToken = IERC20(0xc4AD29ba4B3c580e6D59105FFf484999997675Ff);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(threecrypto), type(uint256).max);\n CurveToken.approve(address(cvx3Crypto), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountIntermediate = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n uint256[3] memory amountsAdded = [amountIntermediate, 0, 0];\n\n threecrypto.add_liquidity(amountsAdded, 0);\n\n uint256 amountTo = CurveToken.balanceOf(address(this));\n\n cvx3Crypto.deposit(amountTo, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(cvx3Crypto, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}" + }, + "contracts/swappers/Leverage/RenCrvLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function add_liquidity(uint256[2] memory amounts, uint256 _min_mint_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n //deposit a curve token\n function deposit(uint256 _amount, address _to) external;\n}\n\ncontract RenCrvLevSwapper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool constant public renCrv = CurvePool(0x93054188d876f558f4a66B2EF1d97d16eDf0895B);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n IConvex public constant cvxRen = IConvex(0xB65eDE134521F0EFD4E943c835F450137dC6E83e);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant CurveToken = IERC20(0x49849C98ae39Fff122806C06791Fa73784FB3675);\n IERC20 public constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(threecrypto), type(uint256).max);\n WBTC.approve(address(renCrv), type(uint256).max);\n CurveToken.approve(address(cvxRen), type(uint256).max);\n }\n\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountOne = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(this));\n\n threecrypto.exchange(0, 1, amountOne, 0);\n\n uint256 amountIntermediate = WBTC.balanceOf(address(this));\n\n uint256[2] memory amountsAdded = [0, amountIntermediate];\n\n renCrv.add_liquidity(amountsAdded, 0);\n\n uint256 amountTo = CurveToken.balanceOf(address(this));\n\n cvxRen.deposit(amountTo, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(cvxRen, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}" + }, + "contracts/PrivatePool.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Cauldron\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./interfaces/IOracle.sol\";\nimport \"./interfaces/ISimpleSwapper.sol\";\nimport \"./libraries/FullMath.sol\";\n\n/// @title PrivatePool\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract PrivatePool is BoringOwnable, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogExchangeRate(uint256 rate);\n event LogAccrue(uint256 accruedAmount, uint256 feeAmount);\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\n event LogAddAsset(address indexed from, uint256 share);\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\n event LogRemoveAsset(address indexed to, uint256 share);\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 openFeeAmount, uint256 part);\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogSeizeCollateral(address indexed from, uint256 collateralShare, uint256 debtAmount, uint256 debtPart);\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(address indexed feeTo, uint256 assetFeeShare, uint256 collateralFeeShare);\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n PrivatePool public immutable masterContract;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC20 public collateral;\n IERC20 public asset;\n IOracle public oracle;\n bytes public oracleData;\n\n // A note on terminology:\n // \"Shares\" are BentoBox shares.\n // \"Parts\" and represent shares held in the debt pool\n\n // The BentoBox balance is the sum of the below two.\n // Since that fits in a single uint128, we can often forgo overflow checks.\n struct AssetBalance {\n uint128 reservesShare;\n uint128 feesEarnedShare;\n }\n AssetBalance public assetBalance;\n uint256 public feesOwedAmount; // Positive only if reservesShare = 0\n\n // The BentoBox balance is the sum of the below two.\n // Seized collateral goes to the \"userCollateralShare\" account of the\n // lender.\n struct CollateralBalance {\n uint128 userTotalShare;\n uint128 feesEarnedShare;\n }\n CollateralBalance public collateralBalance;\n mapping(address => uint256) public userCollateralShare;\n\n // Elastic: Exact asset token amount that currently needs to be repaid\n // Base: Total parts of the debt held by borrowers (borrowerDebtPart)\n Rebase public totalDebt;\n mapping(address => uint256) public borrowerDebtPart;\n\n address public lender;\n mapping(address => bool) public approvedBorrowers;\n\n /// @notice Exchange and interest rate tracking.\n /// This is 'cached' here because calls to Oracles can be very expensive.\n uint256 public exchangeRate;\n\n struct AccrueInfo {\n uint64 lastAccrued;\n uint64 INTEREST_PER_SECOND; // (in units of 1/10^18)\n uint64 EXPIRATION;\n uint16 COLLATERALIZATION_RATE_BPS;\n uint16 LIQUIDATION_MULTIPLIER_BPS;\n uint16 BORROW_OPENING_FEE_BPS;\n bool LIQUIDATION_SEIZE_COLLATERAL;\n }\n AccrueInfo public accrueInfo;\n\n uint256 private constant PROTOCOL_FEE_BPS = 1000; // 10%\n uint256 private constant BPS = 10_000;\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\n\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n masterContract = this;\n }\n\n struct InitSettings {\n IERC20 collateral;\n IERC20 asset;\n IOracle oracle;\n bytes oracleData;\n address lender;\n address[] borrowers;\n uint64 INTEREST_PER_SECOND;\n uint64 EXPIRATION;\n uint16 COLLATERALIZATION_RATE_BPS;\n uint16 LIQUIDATION_MULTIPLIER_BPS;\n uint16 BORROW_OPENING_FEE_BPS;\n bool LIQUIDATION_SEIZE_COLLATERAL;\n }\n\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\n function init(bytes calldata data) public payable override {\n require(address(collateral) == address(0), \"PrivatePool: already initialized\");\n\n InitSettings memory settings = abi.decode(data, (InitSettings));\n require(address(settings.collateral) != address(0), \"PrivatePool: bad pair\");\n require(settings.LIQUIDATION_MULTIPLIER_BPS >= BPS, \"PrivatePool: negative liquidation bonus\");\n require(settings.COLLATERALIZATION_RATE_BPS <= BPS, \"PrivatePool: bad collateralization rate\");\n\n collateral = settings.collateral;\n asset = settings.asset;\n oracle = settings.oracle;\n oracleData = settings.oracleData;\n lender = settings.lender;\n\n AccrueInfo memory _aI;\n _aI.INTEREST_PER_SECOND = settings.INTEREST_PER_SECOND;\n _aI.EXPIRATION = settings.EXPIRATION == 0 ? uint64(-1) : settings.EXPIRATION;\n _aI.COLLATERALIZATION_RATE_BPS = settings.COLLATERALIZATION_RATE_BPS;\n _aI.LIQUIDATION_MULTIPLIER_BPS = settings.LIQUIDATION_MULTIPLIER_BPS;\n _aI.BORROW_OPENING_FEE_BPS = settings.BORROW_OPENING_FEE_BPS;\n _aI.LIQUIDATION_SEIZE_COLLATERAL = settings.LIQUIDATION_SEIZE_COLLATERAL;\n accrueInfo = _aI;\n\n for (uint256 i = 0; i < settings.borrowers.length; i++) {\n approvedBorrowers[settings.borrowers[i]] = true;\n }\n }\n\n function setApprovedBorrowers(address borrower, bool approved) external onlyOwner {\n approvedBorrowers[borrower] = approved;\n }\n\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\n function accrue() public {\n AccrueInfo memory _accrueInfo = accrueInfo;\n // Number of seconds since accrue was called\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\n if (elapsedTime == 0) {\n return;\n }\n accrueInfo.lastAccrued = uint64(block.timestamp);\n\n Rebase memory _totalDebt = totalDebt;\n if (_totalDebt.base == 0) {\n return;\n }\n // No overflow:\n // - _totalDebt.elastic is 128 bits\n // - INTEREST_PER_SECOND is 64 bits\n // - elapsedTime fits in 64 bits for the next 580 billion (10^9) years\n uint256 extraAmount = (uint256(_totalDebt.elastic) * _accrueInfo.INTEREST_PER_SECOND * elapsedTime) / 1e18;\n\n // If the interest rate is too high, then this will overflow and\n // effectively lock up all funds. Do not set the interest rate too\n // high.\n _totalDebt.elastic = _totalDebt.elastic.add(extraAmount.to128());\n totalDebt = _totalDebt;\n\n // No overflow: extraAmount is divided by 1e18 > 2^59; we need 16 bits\n uint256 feeAmount = (extraAmount * PROTOCOL_FEE_BPS) / BPS;\n\n AssetBalance memory _assetBalance = assetBalance;\n if (_assetBalance.reservesShare == 0) {\n // Fees owed are always part of the debt, and the debt just got\n // at least `feeAmount` added to it. If that fit, so does this:\n feesOwedAmount += feeAmount;\n } else {\n uint256 feeShare = bentoBox.toShare(asset, feeAmount, false);\n if (_assetBalance.reservesShare < feeShare) {\n _assetBalance.feesEarnedShare += _assetBalance.reservesShare;\n feesOwedAmount += bentoBox.toAmount(asset, feeShare - _assetBalance.reservesShare, false);\n _assetBalance.reservesShare = 0;\n } else {\n // feesEarned + fee <= feesEarned + reserves <= Bento balance:\n _assetBalance.reservesShare -= uint128(feeShare);\n _assetBalance.feesEarnedShare += uint128(feeShare);\n }\n assetBalance = _assetBalance;\n }\n\n emit LogAccrue(extraAmount, feeAmount);\n }\n\n function _isSolvent(address borrower) internal returns (bool) {\n (, uint256 _exchangeRate) = updateExchangeRate();\n\n // accrue must have already been called!\n uint256 debtPart = borrowerDebtPart[borrower];\n if (debtPart == 0) return true;\n uint256 collateralShare = userCollateralShare[borrower];\n if (collateralShare == 0) return false;\n\n // The inequality that needs to hold for a user to be solvent is:\n //\n // (value of user collateral) * (max LTV) >= (value of user debt)\n //\n // Our exchange rates give \"collateral per ether of asset\", so it makes\n // sense to express the value in the collateral token. The calculation\n // for the collateral value is:\n //\n // BentoBox tokens\n // value = shares * ---------------\n // BentoBox shares\n //\n // where BentoBox tokens resp. shares refer to the balances for the\n // collateral (ERC20) contract. For the debt we get:\n //\n // Total debt tokens Collateral wei per asset ether\n // value = parts * ----------------- * ------------------------------\n // Total debt parts 1e18\n //\n // ..since \"tokens\" is in wei. We call this EXCHANGE_RATE_PRECISION.\n // Finally, max LTV is\n //\n // COLLATERALIZATION_RATE_BPS\n // ratio = --------------------------.\n // 1e4\n //\n // We use the below table to (fit the inequality in 80 characters and)\n // move some things around and justify our calculations.\n //\n // Description Variable in contract Bits\n // -----------------------------------------------------------------\n // cs shares collateralShare 128*\n // Be BentoBox tokens bentoBoxTotals.elastic 128\n // Bb BentoBox shares bentoBoxTotals.base 128\n // -----------------------------------------------------------------\n // dp parts debtPart 128*\n // De Total debt tokens totalDebt.elastic 128\n // Db Total debt parts totalDebt.base 128\n // xr Coll. wei per asset ether exchangeRate 256\n // 1e18 EXCHANGE_RATE_PRECISION 60\n // ----------------------------------------------------------------\n // ltv COLLATERALIZATION_RATE_BPS 14\n // 1e4 BPS 14\n // 1e14 47\n //\n // (* as in other proofs, these values fit in some 128-bit total).\n // The inequality, without regard for integer division:\n //\n // Be ltv De xr\n // cs * -- * --- >= dp * -- * ----\n // Bb 1e4 Db 1e18\n //\n // This is equivalent to:\n //\n // cs * Be * ltv * Db * 1e14 >= dp * De * xr * Bb\n //\n // Corresponding bit counts:\n //\n // 128 + 128 + 14 + 128 + 47; 128 + 128 + 256 + 128\n //\n // So, the LHS definitely fits in 512 bits, and as long as one token is\n // not 2^68 times as valuable as another. Of course the other terms\n // leave some leeway, too; if the exchange rate is this high, then the\n // Bento share count is very unlikely to take up the full 128 bits.\n //\n // \"Full\" multiplication is not very expensive or cumbersome; the case\n // where `exchangeRate` is too big is a little more involved, but\n // manageable.\n\n Rebase memory _totalDebt = totalDebt;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n\n (uint256 leftLo, uint256 leftHi) = FullMath.fullMul(\n collateralShare * bentoBoxTotals.elastic,\n // Cast needed to avoid uint128 overflow\n uint256(accrueInfo.COLLATERALIZATION_RATE_BPS) * _totalDebt.base * 1e14\n );\n uint256 rightLo;\n uint256 rightHi;\n if (_exchangeRate <= type(uint128).max) {\n (rightLo, rightHi) = FullMath.fullMul(debtPart * _totalDebt.elastic, _exchangeRate * bentoBoxTotals.base);\n } else {\n // We multiply it out in stages to be safe. If the total overflows\n // 512 bits then we are done, as the LHS is guaranteed to be less.\n //\n // aHi * 2^256 + aLo = dp * De * Bb\n // -----------------------------------------------------------------\n // The total is then the sum of:\n //\n // bHi * 2^512 + bLo * 2^256 = xr * aHi * 2^256\n // cHi * 2^256 + cLo = xr * aLo\n //\n (uint256 aLo, uint256 aHi) = FullMath.fullMul(debtPart * _totalDebt.elastic, bentoBoxTotals.base);\n (uint256 bLo, uint256 bHi) = FullMath.fullMul(_exchangeRate, aHi);\n if (bHi > 0) {\n return false;\n }\n\n uint256 cHi; // (cLo is rightLo)\n (rightLo, cHi) = FullMath.fullMul(_exchangeRate, aLo);\n rightHi = cHi + bLo;\n if (rightHi < cHi) {\n return false;\n }\n }\n return leftHi > rightHi || (leftHi == rightHi && leftLo >= rightLo);\n }\n\n /// @dev Checks if the borrower is solvent in the closed liquidation case at the end of the function body.\n modifier solvent() {\n _;\n require(_isSolvent(msg.sender), \"PrivatePool: borrower insolvent\");\n }\n\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\n /// @return updated True if `exchangeRate` was updated.\n /// @return rate The new exchange rate.\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\n (updated, rate) = oracle.get(oracleData);\n\n if (updated) {\n exchangeRate = rate;\n emit LogExchangeRate(rate);\n } else {\n // Return the old rate if fetching wasn't successful\n rate = exchangeRate;\n }\n }\n\n /// @dev Helper function to move tokens.\n /// @param token The ERC-20 token.\n /// @param share The amount in shares to add.\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\n /// Only used for accounting checks.\n /// @param skim If True, only does a balance check on this contract.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n function _addTokens(\n IERC20 token,\n uint256 share,\n uint256 total,\n bool skim\n ) internal {\n if (skim) {\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"PrivatePool: skim too much\");\n } else {\n bentoBox.transfer(token, msg.sender, address(this), share);\n }\n }\n\n /// @notice Adds `collateral` from msg.sender to the account `to`.\n /// @param to The receiver of the tokens.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add for `to`.\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) public {\n uint256 supplied = userCollateralShare[to];\n require(supplied > 0 || approvedBorrowers[to], \"PrivatePool: unapproved borrower\");\n\n // No overflow: the total for ALL users fits in 128 bits, or the\n // BentoBox transfer (_addTokens) fails.\n userCollateralShare[to] = supplied + share;\n CollateralBalance memory _collateralBalance = collateralBalance;\n // No overflow: the sum fits in the BentoBox total\n uint256 prevTotal = _collateralBalance.userTotalShare + _collateralBalance.feesEarnedShare;\n // No overflow if cast safe: fits in the BentoBox or _addTokens reverts\n // Cast safe: _addTokens does not truncate the value\n collateralBalance.userTotalShare = _collateralBalance.userTotalShare + uint128(share);\n _addTokens(collateral, share, prevTotal, skim);\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\n }\n\n /// @dev Concrete implementation of `removeCollateral`.\n function _removeCollateral(address to, uint256 share) internal {\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\n // No underflow: userTotalShare > userCollateralShare[msg.sender]\n // Cast safe: Bento transfer reverts if it is not.\n collateralBalance.userTotalShare -= uint128(share);\n emit LogRemoveCollateral(msg.sender, to, share);\n bentoBox.transfer(collateral, address(this), to, share);\n }\n\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\n /// @param to The receiver of the shares.\n /// @param share Amount of shares to remove.\n function removeCollateral(address to, uint256 share) public solvent {\n // accrue must be called because we check solvency\n accrue();\n _removeCollateral(to, share);\n }\n\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// @param toReservesShare Amount of shares to reserves.\n /// @param toReservesAmount Token amount. Ignored if `toReservesShare` nonzero.\n /// @param toFeesAmount Token fee amount. Ignored if `toReservesShare` nonzero.\n function _receiveAsset(\n bool skim,\n uint256 toReservesShare,\n // (There is no case where we pass along a fee in shares)\n uint256 toReservesAmount,\n uint256 toFeesAmount\n ) internal {\n IERC20 _asset = asset;\n AssetBalance memory _assetBalance = assetBalance;\n uint256 priorAssetTotalShare = _assetBalance.reservesShare + _assetBalance.feesEarnedShare;\n Rebase memory bentoBoxTotals = bentoBox.totals(_asset);\n\n uint256 toFeesShare = 0;\n if (toReservesShare == 0) {\n toReservesShare = bentoBoxTotals.toBase(toReservesAmount, true);\n if (toFeesAmount > 0) {\n toFeesShare = bentoBoxTotals.toBase(toFeesAmount, false);\n }\n }\n uint256 takenShare = toReservesShare.add(toFeesShare);\n\n if (_assetBalance.reservesShare == 0) {\n uint256 _feesOwedAmount = feesOwedAmount;\n if (_feesOwedAmount > 0) {\n uint256 feesOwedShare = bentoBoxTotals.toBase(_feesOwedAmount, false);\n // New fees cannot pay off existing fees:\n if (toReservesShare < feesOwedShare) {\n feesOwedAmount = bentoBoxTotals.toElastic(feesOwedShare - toReservesShare, false);\n _assetBalance.feesEarnedShare += uint128(takenShare);\n } else {\n feesOwedAmount = 0;\n // No overflow: assuming the transfer at the end succeeds:\n // feesOwedShare <= toReservesShare <= (Bento balance),\n _assetBalance.feesEarnedShare += uint128(feesOwedShare + toFeesShare);\n _assetBalance.reservesShare = uint128(toReservesShare - feesOwedShare);\n }\n } else {\n _assetBalance.reservesShare = uint128(toReservesShare);\n _assetBalance.feesEarnedShare += uint128(toFeesShare);\n }\n } else {\n _assetBalance.reservesShare += uint128(toReservesShare);\n _assetBalance.feesEarnedShare += uint128(toFeesShare);\n }\n assetBalance = _assetBalance;\n\n _addTokens(_asset, takenShare, priorAssetTotalShare, skim);\n }\n\n /// @dev Concrete implementation of `addAsset`.\n function _addAsset(bool skim, uint256 share) internal {\n _receiveAsset(skim, share, 0, 0);\n emit LogAddAsset(skim ? address(bentoBox) : msg.sender, share);\n }\n\n /// @notice Adds assets to the lending pair.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add.\n function addAsset(bool skim, uint256 share) public {\n accrue();\n _addAsset(skim, share);\n }\n\n /// @dev Concrete implementation of `removeAsset`.\n function _removeAsset(address to, uint256 share) internal {\n require(msg.sender == lender, \"PrivatePool: not the lender\");\n // Cast safe: Bento transfer reverts unless stronger condition holds\n assetBalance.reservesShare = assetBalance.reservesShare.sub(uint128(share));\n bentoBox.transfer(asset, address(this), to, share);\n emit LogRemoveAsset(to, share);\n }\n\n /// @notice Removes an asset from msg.sender and transfers it to `to`.\n /// @param to The address that receives the removed assets.\n /// @param share The amount of shares to remove.\n function removeAsset(address to, uint256 share) public {\n accrue();\n _removeAsset(to, share);\n }\n\n /// @dev Concrete implementation of `borrow`.\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\n require(approvedBorrowers[msg.sender], \"PrivatePool: unapproved borrower\");\n IERC20 _asset = asset;\n Rebase memory bentoBoxTotals = bentoBox.totals(_asset);\n AccrueInfo memory _accrueInfo = accrueInfo;\n\n share = bentoBoxTotals.toBase(amount, false);\n\n // No overflow: `share` is not modified any more, and fits in the\n // BentoBox shares total if the transfer succeeds. But then \"amount\"\n // must fit in the token total; at least up to some rounding error,\n // which cannot be more than 128 bits either.\n uint256 openFeeAmount = (amount * _accrueInfo.BORROW_OPENING_FEE_BPS) / BPS;\n // No overflow: Same reason. Also, we just divided by BPS..\n uint256 protocolFeeAmount = (openFeeAmount * PROTOCOL_FEE_BPS) / BPS;\n uint256 protocolFeeShare = bentoBoxTotals.toBase(protocolFeeAmount, false);\n\n // The protocol component of the opening fee cannot be owed:\n AssetBalance memory _assetBalance = assetBalance;\n // No overflow on the add: protocolFeeShare < share < Bento total, or\n // the transfer reverts. The transfer is independent of the results of\n // these calculations: `share` is not modified.\n // Theoretically the fee could just make it overflow 128 bits.\n // Underflow check is core business logic:\n _assetBalance.reservesShare = _assetBalance.reservesShare.sub((share + protocolFeeShare).to128());\n // Cast is safe: `share` fits. Also, the checked cast above succeeded.\n // No overflow: protocolFeeShare < reservesShare, and both balances\n // together fit in the Bento share balance,\n _assetBalance.feesEarnedShare += uint128(protocolFeeShare);\n assetBalance = _assetBalance;\n\n // No overflow (inner add): amount fits in 128 bits, as shown before,\n // and openFeeAmount is less.\n (totalDebt, part) = totalDebt.add(amount + openFeeAmount, true);\n // No overflow: totalDebt.base is the sum of all borrowerDebtParts,\n // fits in 128 bits and just had \"part\" added to it.\n borrowerDebtPart[msg.sender] += part;\n emit LogBorrow(msg.sender, to, amount, openFeeAmount, part);\n\n bentoBox.transfer(_asset, address(this), to, share);\n }\n\n /// @notice Sender borrows `amount` and transfers it to `to`.\n /// @return part Total part of the debt held by borrowers.\n /// @return share Total amount in shares borrowed.\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\n accrue();\n (part, share) = _borrow(to, amount);\n }\n\n /// @dev Concrete implementation of `repay`.\n function _repay(\n address to,\n bool skim,\n uint256 part\n ) internal returns (uint256 amount) {\n (totalDebt, amount) = totalDebt.sub(part, true);\n borrowerDebtPart[to] = borrowerDebtPart[to].sub(part);\n _receiveAsset(skim, 0, amount, 0);\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\n }\n\n /// @notice Repays a loan.\n /// @param to Address of the borrower this payment should go.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param part The amount to repay as part. See `borrowerDebtPart`.\n /// @return amount The total amount repayed.\n function repay(\n address to,\n bool skim,\n uint256 part\n ) public returns (uint256 amount) {\n accrue();\n amount = _repay(to, skim, part);\n }\n\n // Functions that need accrue to be called\n uint8 internal constant ACTION_ADD_ASSET = 1;\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_ASSET = 3;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n uint8 internal constant ACTION_BORROW = 5;\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\n uint8 internal constant ACTION_ACCRUE = 8;\n\n // Functions that don't need accrue to be called\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode(\n data,\n (address, bytes, bool, bool, uint8)\n );\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(callee != address(bentoBox) && callee != address(this), \"PrivatePool: can't call\");\n\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\n require(success, \"PrivatePool: call failed\");\n return (returnData, returnValues);\n }\n\n struct CookStatus {\n bool needsSolvencyCheck;\n bool hasAccrued;\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n CookStatus memory status;\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (!status.hasAccrued && action < 10) {\n accrue();\n status.hasAccrued = true;\n }\n if (action == ACTION_ADD_COLLATERAL) {\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n addCollateral(to, skim, _num(share, value1, value2));\n } else if (action == ACTION_ADD_ASSET) {\n (int256 share, bool skim) = abi.decode(datas[i], (int256, bool));\n _addAsset(skim, _num(share, value1, value2));\n } else if (action == ACTION_REPAY) {\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n _repay(to, skim, _num(part, value1, value2));\n } else if (action == ACTION_REMOVE_ASSET) {\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\n _removeAsset(to, _num(share, value1, value2));\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\n _removeCollateral(to, _num(share, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_BORROW) {\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\n (bool updated, uint256 rate) = updateExchangeRate();\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"PrivatePool: rate not ok\");\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode(\n datas[i],\n (address, address, bool, uint8, bytes32, bytes32)\n );\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\n }\n } else if (action == ACTION_GET_REPAY_SHARE) {\n int256 part = abi.decode(datas[i], (int256));\n value1 = bentoBox.toShare(asset, totalDebt.toElastic(_num(part, value1, value2), true), true);\n } else if (action == ACTION_GET_REPAY_PART) {\n int256 amount = abi.decode(datas[i], (int256));\n value1 = totalDebt.toBase(_num(amount, value1, value2), false);\n }\n }\n\n if (status.needsSolvencyCheck) {\n require(_isSolvent(msg.sender), \"PrivatePool: borrower insolvent\");\n }\n }\n\n /// @notice Handles the liquidation of borrowers' balances, once the borrowers' amount of collateral is too low.\n /// @param borrowers An array of borrower addresses.\n /// @param maxDebtParts A one-to-one mapping to `borrowers`, contains maximum part (not token amount) of the debt that will be liquidated of the respective borrower.\n /// @param to Address of the receiver if `swapper` is zero.\n /// @param swapper Contract address of the `ISimpleSwapper` implementation.\n function liquidate(\n address[] calldata borrowers,\n uint256[] calldata maxDebtParts,\n address to,\n ISimpleSwapper swapper\n ) public {\n // Oracle can fail but we still need to allow liquidations\n (, uint256 _exchangeRate) = updateExchangeRate();\n accrue();\n\n AccrueInfo memory _accrueInfo = accrueInfo;\n\n uint256 allCollateralShare;\n uint256 allDebtAmount;\n uint256 allDebtPart;\n Rebase memory _totalDebt = totalDebt;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n\n for (uint256 i = 0; i < borrowers.length; i++) {\n // TODO: Find a way to not have it here; only for stack reasons\n address borrower = borrowers[i];\n // If we set an expiration at all, then by the above check it is\n // now past and every borrower can be liquidated at the current\n // price:\n if (block.timestamp >= accrueInfo.EXPIRATION || !_isSolvent(borrower)) {\n uint256 debtPart;\n {\n uint256 availableDebtPart = borrowerDebtPart[borrower];\n debtPart = maxDebtParts[i] > availableDebtPart ? availableDebtPart : maxDebtParts[i];\n // No underflow: ensured by definition of debtPart\n borrowerDebtPart[borrower] = availableDebtPart - debtPart;\n }\n uint256 debtAmount = _totalDebt.toElastic(debtPart, false);\n // No overflow (inner): debtAmount <= totalDebt.elastic < 2^128.\n // The exchange rate need not be reasonable: with an expiration\n // time set there is no _isSolvent() call.\n uint256 collateralShare = bentoBoxTotals.toBase(\n (debtAmount * _accrueInfo.LIQUIDATION_MULTIPLIER_BPS).mul(_exchangeRate) / (BPS * EXCHANGE_RATE_PRECISION),\n false\n );\n\n // This needs to be updated here so that the same user cannot\n // be liquidated more than once. (Unless it adds up to one\n // \"full\" liquidation or less).\n // Underflow check is business logic: the liquidator can only\n // take enough to cover the loan (and bonus).\n userCollateralShare[borrower] = userCollateralShare[borrower].sub(collateralShare);\n\n if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) {\n emit LogSeizeCollateral(borrower, collateralShare, debtAmount, debtPart);\n } else {\n emit LogRemoveCollateral(borrower, swapper == ISimpleSwapper(0) ? to : address(swapper), collateralShare);\n emit LogRepay(swapper == ISimpleSwapper(0) ? msg.sender : address(swapper), borrower, debtAmount, debtPart);\n }\n\n // No overflow in the below three:\n //\n // share(s) / amount(s) / part(s) involved in liquidation\n // <= total share / amount / part\n // <= (Bento).base / totalDebt.elastic / totalDebt.base\n // < 2^128\n //\n // Collateral share and debt part have already been\n // successfully subtracted from some user's balance (and this\n // persists across loop runs); the calculation for debt amount\n // rounds down, so it fits if debtPart fits. It follows that\n // the condition holds for the accumulated sums.\n allCollateralShare += collateralShare;\n allDebtAmount += debtAmount;\n allDebtPart += debtPart;\n }\n }\n require(allDebtAmount != 0, \"PrivatePool: all are solvent\");\n // No overflow (both): (liquidated debt) <= (total debt).\n // Cast is safe (both): (liquidated debt) <= (total debt) < 2^128\n _totalDebt.elastic -= uint128(allDebtAmount);\n _totalDebt.base -= uint128(allDebtPart);\n totalDebt = _totalDebt;\n\n if (_accrueInfo.LIQUIDATION_SEIZE_COLLATERAL) {\n // Unlike normal liquidations, the liquidator and the lender share\n // the excess. This compensates the lender for agreeing to payment\n // in the collateral. The protocol gets a cut of the total excess.\n // So the final distribution is\n // - X% excess (configurable via LIQUIDATION_MULTIPLIER_BPS)\n // - (10% of X) protocol fee\n // - (45% of X) liquidator share of bonus\n // - 100% + (45% of X) debt + lender share of bonus\n // allCollateralShare already includes the bonus, which in turn\n // includes the protocol fee. We round the bonus down to favor the\n // lender, and the fee to favor the liquidator and lender:\n // Math: All collateral fits in 128 bits (BentoBox), so the\n // multiplications are safe:\n uint256 excessShare = (allCollateralShare * (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS)) /\n _accrueInfo.LIQUIDATION_MULTIPLIER_BPS;\n uint256 feeShare = (excessShare * PROTOCOL_FEE_BPS) / BPS;\n uint256 liquidatorShare = (excessShare - feeShare) / 2;\n // We would add more variables for clarity, but stack depth:\n {\n CollateralBalance memory _collateralBalance = collateralBalance;\n // No underflow: All amounts fit in the collateral Bento total\n // The lender is also a \"user\", so only the fee and liquidator\n // share leave the user total \"account\":\n _collateralBalance.userTotalShare -= uint128(feeShare + liquidatorShare);\n _collateralBalance.feesEarnedShare += uint128(feeShare);\n collateralBalance = _collateralBalance;\n }\n // The rest goes to the lender:\n userCollateralShare[lender] += (allCollateralShare - feeShare - liquidatorShare);\n // The liquidator gets the other half -- rounded in their favour,\n // if applicable:\n bentoBox.transfer(collateral, address(this), to, liquidatorShare);\n } else {\n // No underflow: allCollateralShare is the sum of quantities that\n // have successfully been taken out of user balances.\n // Cast is safe: Above reason, and userTotalShare < 2^128\n collateralBalance.userTotalShare -= uint128(allCollateralShare);\n\n // Charge the protocol fee over the excess.\n // No overflow:\n // allDebtAmount <= totalDebt.elastic < 2^128 (proof in loop)\n // LIQUIDATION_MULTIPLIER_BPS < 2^16\n // PROTOCOL_FEE_BPS <= 10k < 2^14 (or we have bigger problems)\n uint256 feeAmount = (allDebtAmount * (_accrueInfo.LIQUIDATION_MULTIPLIER_BPS - BPS) * PROTOCOL_FEE_BPS) / (BPS * BPS);\n\n // Swap using a swapper freely chosen by the caller\n // Open (flash) liquidation: get proceeds first and provide the\n // borrow after\n bentoBox.transfer(collateral, address(this), swapper == ISimpleSwapper(0) ? to : address(swapper), allCollateralShare);\n if (swapper != ISimpleSwapper(0)) {\n // TODO: Somehow split _receiveAsset to reduce loads?\n IERC20 _asset = asset;\n swapper.swap(\n collateral,\n _asset,\n msg.sender,\n bentoBox.toShare(_asset, allDebtAmount.add(feeAmount), true),\n allCollateralShare\n );\n }\n _receiveAsset(false, 0, allDebtAmount, feeAmount);\n }\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n accrue();\n address to = masterContract.feeTo();\n\n uint256 assetShare = assetBalance.feesEarnedShare;\n if (assetShare > 0) {\n bentoBox.transfer(asset, address(this), to, assetShare);\n assetBalance.feesEarnedShare = 0;\n }\n\n uint256 collateralShare = collateralBalance.feesEarnedShare;\n if (collateralShare > 0) {\n bentoBox.transfer(collateral, address(this), to, collateralShare);\n collateralBalance.feesEarnedShare = 0;\n }\n\n emit LogWithdrawFees(to, assetShare, collateralShare);\n }\n\n /// @notice Sets the beneficiary of fees accrued in liquidations.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n}\n" + }, + "contracts/interfaces/ISimpleSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\n\ninterface ISimpleSwapper {\n /// @notice Withdraws 'amountFrom' of token 'from' from the BentoBox account for this swapper.\n /// Swaps it for at least 'amountToMin' of token 'to'.\n /// Transfers the swapped tokens of 'to' into the BentoBox using a plain ERC20 transfer.\n /// Returns the amount of tokens 'to' transferred to BentoBox.\n /// (The BentoBox skim function will be used by the caller to get the swapped funds).\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) external returns (uint256 extraShare, uint256 shareReturned);\n}\n" + }, + "contracts/libraries/FullMath.sol": { + "content": "// SPDX-License-Identifier: CC-BY-4.0\npragma solidity 0.6.12;\n\n// solhint-disable\n\n// taken from https://medium.com/coinmonks/math-in-solidity-part-3-percents-and-proportions-4db014e080b1\n// license is CC-BY-4.0\nlibrary FullMath {\n function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) {\n uint256 mm = mulmod(x, y, uint256(-1));\n l = x * y;\n h = mm - l;\n if (mm < l) h -= 1;\n }\n\n function fullDiv(\n uint256 l,\n uint256 h,\n uint256 d\n ) private pure returns (uint256) {\n uint256 pow2 = d & -d;\n d /= pow2;\n l /= pow2;\n l += h * ((-pow2) / pow2 + 1);\n uint256 r = 1;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n r *= 2 - d * r;\n return l * r;\n }\n\n function mulDiv(\n uint256 x,\n uint256 y,\n uint256 d\n ) internal pure returns (uint256) {\n (uint256 l, uint256 h) = fullMul(x, y);\n uint256 mm = mulmod(x, y, d);\n if (mm > l) h -= 1;\n l -= mm;\n require(h < d, \"FullMath::mulDiv: overflow\");\n return fullDiv(l, h, d);\n }\n}\n" + }, + "contracts/libraries/FixedPoint.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0-or-later\npragma solidity 0.6.12;\nimport \"./FullMath.sol\";\n\n// solhint-disable\n\n// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))\nlibrary FixedPoint {\n // range: [0, 2**112 - 1]\n // resolution: 1 / 2**112\n struct uq112x112 {\n uint224 _x;\n }\n\n // range: [0, 2**144 - 1]\n // resolution: 1 / 2**112\n struct uq144x112 {\n uint256 _x;\n }\n\n uint8 private constant RESOLUTION = 112;\n uint256 private constant Q112 = 0x10000000000000000000000000000;\n uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000;\n uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits)\n\n // decode a UQ144x112 into a uint144 by truncating after the radix point\n function decode144(uq144x112 memory self) internal pure returns (uint144) {\n return uint144(self._x >> RESOLUTION);\n }\n\n // multiply a UQ112x112 by a uint256, returning a UQ144x112\n // reverts on overflow\n function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) {\n uint256 z = 0;\n require(y == 0 || (z = self._x * y) / y == self._x, \"FixedPoint::mul: overflow\");\n return uq144x112(z);\n }\n\n // returns a UQ112x112 which represents the ratio of the numerator to the denominator\n // lossy if either numerator or denominator is greater than 112 bits\n function fraction(uint256 numerator, uint256 denominator) internal pure returns (uq112x112 memory) {\n require(denominator > 0, \"FixedPoint::fraction: div by 0\");\n if (numerator == 0) return FixedPoint.uq112x112(0);\n\n if (numerator <= uint144(-1)) {\n uint256 result = (numerator << RESOLUTION) / denominator;\n require(result <= uint224(-1), \"FixedPoint::fraction: overflow\");\n return uq112x112(uint224(result));\n } else {\n uint256 result = FullMath.mulDiv(numerator, Q112, denominator);\n require(result <= uint224(-1), \"FixedPoint::fraction: overflow\");\n return uq112x112(uint224(result));\n }\n }\n}\n" + }, + "contracts/oracles/xJOEOracle.sol": { + "content": "// SPDX-License-Identifier: AGPL-3.0-only\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../libraries/FixedPoint.sol\";\n\nimport \"hardhat/console.sol\";\n\n// solhint-disable not-rely-on-time\n\n// adapted from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ncontract XJoeOracleV2 is IOracle {\n using FixedPoint for *;\n using BoringMath for uint256;\n uint256 public constant PERIOD = 10 minutes;\n IAggregator public constant AVAX_USD = IAggregator(0x0A77230d17318075983913bC2145DB16C7366156);\n IUniswapV2Pair public constant JOE_AVAX = IUniswapV2Pair(0x454E67025631C065d3cFAD6d71E6892f74487a15);\n IERC20 public constant JOE = IERC20(0x6e84a6216eA6dACC71eE8E6b0a5B7322EEbC0fDd);\n IERC20 public constant XJOE = IERC20(0x57319d41F71E81F3c65F2a47CA4e001EbAFd4F33);\n\n struct PairInfo {\n uint256 priceCumulativeLast;\n uint32 blockTimestampLast;\n uint144 priceAverage;\n }\n\n PairInfo public pairInfo;\n function _get(uint32 blockTimestamp) public view returns (uint256) {\n uint256 priceCumulative = JOE_AVAX.price0CumulativeLast();\n\n // if time has elapsed since the last update on the pair, mock the accumulated price values\n (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(JOE_AVAX).getReserves();\n priceCumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * (blockTimestamp - blockTimestampLast); // overflows ok\n\n // overflow is desired, casting never truncates\n // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed\n return priceCumulative;\n }\n\n function toXJOE(uint256 amount) internal view returns (uint256) {\n return amount.mul(JOE.balanceOf(address(XJOE))) / XJOE.totalSupply();\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata) external override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = _get(blockTimestamp);\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n console.log(timeElapsed);\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n pairInfo.priceAverage = uint144(1e44 / toXJOE(uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144())).mul(uint256(AVAX_USD.latestAnswer())));\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = priceCumulative;\n\n return (true, pairInfo.priceAverage);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata) public view override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n uint144 priceAverage = uint144(1e44 / toXJOE(uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144())).mul(uint256(AVAX_USD.latestAnswer())));\n\n return (true, priceAverage);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata) external view override returns (uint256 rate) {\n (uint256 reserve0, uint256 reserve1, ) = JOE_AVAX.getReserves();\n rate = 1e44 / toXJOE(reserve1.mul(1e18) / reserve0).mul(uint256(AVAX_USD.latestAnswer()));\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"xJOE TWAP\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"xJOE\";\n }\n}\n" + }, + "hardhat/console.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity >= 0.4.22 <0.9.0;\n\nlibrary console {\n\taddress constant CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67);\n\n\tfunction _sendLogPayload(bytes memory payload) private view {\n\t\tuint256 payloadLength = payload.length;\n\t\taddress consoleAddress = CONSOLE_ADDRESS;\n\t\tassembly {\n\t\t\tlet payloadStart := add(payload, 32)\n\t\t\tlet r := staticcall(gas(), consoleAddress, payloadStart, payloadLength, 0, 0)\n\t\t}\n\t}\n\n\tfunction log() internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log()\"));\n\t}\n\n\tfunction logInt(int p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(int)\", p0));\n\t}\n\n\tfunction logUint(uint p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint)\", p0));\n\t}\n\n\tfunction logString(string memory p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string)\", p0));\n\t}\n\n\tfunction logBool(bool p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool)\", p0));\n\t}\n\n\tfunction logAddress(address p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address)\", p0));\n\t}\n\n\tfunction logBytes(bytes memory p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes)\", p0));\n\t}\n\n\tfunction logBytes1(bytes1 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes1)\", p0));\n\t}\n\n\tfunction logBytes2(bytes2 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes2)\", p0));\n\t}\n\n\tfunction logBytes3(bytes3 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes3)\", p0));\n\t}\n\n\tfunction logBytes4(bytes4 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes4)\", p0));\n\t}\n\n\tfunction logBytes5(bytes5 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes5)\", p0));\n\t}\n\n\tfunction logBytes6(bytes6 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes6)\", p0));\n\t}\n\n\tfunction logBytes7(bytes7 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes7)\", p0));\n\t}\n\n\tfunction logBytes8(bytes8 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes8)\", p0));\n\t}\n\n\tfunction logBytes9(bytes9 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes9)\", p0));\n\t}\n\n\tfunction logBytes10(bytes10 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes10)\", p0));\n\t}\n\n\tfunction logBytes11(bytes11 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes11)\", p0));\n\t}\n\n\tfunction logBytes12(bytes12 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes12)\", p0));\n\t}\n\n\tfunction logBytes13(bytes13 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes13)\", p0));\n\t}\n\n\tfunction logBytes14(bytes14 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes14)\", p0));\n\t}\n\n\tfunction logBytes15(bytes15 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes15)\", p0));\n\t}\n\n\tfunction logBytes16(bytes16 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes16)\", p0));\n\t}\n\n\tfunction logBytes17(bytes17 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes17)\", p0));\n\t}\n\n\tfunction logBytes18(bytes18 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes18)\", p0));\n\t}\n\n\tfunction logBytes19(bytes19 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes19)\", p0));\n\t}\n\n\tfunction logBytes20(bytes20 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes20)\", p0));\n\t}\n\n\tfunction logBytes21(bytes21 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes21)\", p0));\n\t}\n\n\tfunction logBytes22(bytes22 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes22)\", p0));\n\t}\n\n\tfunction logBytes23(bytes23 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes23)\", p0));\n\t}\n\n\tfunction logBytes24(bytes24 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes24)\", p0));\n\t}\n\n\tfunction logBytes25(bytes25 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes25)\", p0));\n\t}\n\n\tfunction logBytes26(bytes26 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes26)\", p0));\n\t}\n\n\tfunction logBytes27(bytes27 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes27)\", p0));\n\t}\n\n\tfunction logBytes28(bytes28 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes28)\", p0));\n\t}\n\n\tfunction logBytes29(bytes29 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes29)\", p0));\n\t}\n\n\tfunction logBytes30(bytes30 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes30)\", p0));\n\t}\n\n\tfunction logBytes31(bytes31 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes31)\", p0));\n\t}\n\n\tfunction logBytes32(bytes32 p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bytes32)\", p0));\n\t}\n\n\tfunction log(uint p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint)\", p0));\n\t}\n\n\tfunction log(string memory p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string)\", p0));\n\t}\n\n\tfunction log(bool p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool)\", p0));\n\t}\n\n\tfunction log(address p0) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address)\", p0));\n\t}\n\n\tfunction log(uint p0, uint p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint)\", p0, p1));\n\t}\n\n\tfunction log(uint p0, string memory p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string)\", p0, p1));\n\t}\n\n\tfunction log(uint p0, bool p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool)\", p0, p1));\n\t}\n\n\tfunction log(uint p0, address p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address)\", p0, p1));\n\t}\n\n\tfunction log(string memory p0, uint p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint)\", p0, p1));\n\t}\n\n\tfunction log(string memory p0, string memory p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string)\", p0, p1));\n\t}\n\n\tfunction log(string memory p0, bool p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool)\", p0, p1));\n\t}\n\n\tfunction log(string memory p0, address p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address)\", p0, p1));\n\t}\n\n\tfunction log(bool p0, uint p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint)\", p0, p1));\n\t}\n\n\tfunction log(bool p0, string memory p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string)\", p0, p1));\n\t}\n\n\tfunction log(bool p0, bool p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool)\", p0, p1));\n\t}\n\n\tfunction log(bool p0, address p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address)\", p0, p1));\n\t}\n\n\tfunction log(address p0, uint p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint)\", p0, p1));\n\t}\n\n\tfunction log(address p0, string memory p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string)\", p0, p1));\n\t}\n\n\tfunction log(address p0, bool p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool)\", p0, p1));\n\t}\n\n\tfunction log(address p0, address p1) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address)\", p0, p1));\n\t}\n\n\tfunction log(uint p0, uint p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, uint p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, uint p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, uint p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, string memory p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, string memory p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, string memory p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, string memory p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, bool p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, bool p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, bool p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, bool p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, address p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, address p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, address p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, address p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, uint p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, uint p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, uint p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, uint p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, string memory p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, string memory p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, string memory p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, string memory p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, bool p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, bool p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, bool p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, bool p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, address p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, address p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, address p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(string memory p0, address p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, uint p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, uint p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, uint p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, uint p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, string memory p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, string memory p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, string memory p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, string memory p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, bool p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, bool p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, bool p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, bool p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, address p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, address p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, address p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(bool p0, address p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, uint p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, uint p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, uint p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, uint p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, string memory p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, string memory p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, string memory p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, string memory p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, bool p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, bool p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, bool p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, bool p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, address p1, uint p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,uint)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, address p1, string memory p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,string)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, address p1, bool p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,bool)\", p0, p1, p2));\n\t}\n\n\tfunction log(address p0, address p1, address p2) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,address)\", p0, p1, p2));\n\t}\n\n\tfunction log(uint p0, uint p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, uint p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,uint,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, string memory p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,string,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, bool p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,bool,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(uint p0, address p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(uint,address,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, uint p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,uint,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, string memory p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,string,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, bool p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,bool,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(string memory p0, address p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(string,address,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, uint p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,uint,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, string memory p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,string,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, bool p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,bool,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(bool p0, address p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(bool,address,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, uint p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,uint,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, string memory p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,string,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, bool p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,bool,address,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, uint p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,uint,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, uint p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,uint,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, uint p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,uint,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, uint p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,uint,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, string memory p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,string,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, string memory p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,string,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, string memory p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,string,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, string memory p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,string,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, bool p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,bool,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, bool p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,bool,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, bool p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,bool,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, bool p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,bool,address)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, address p2, uint p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,address,uint)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, address p2, string memory p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,address,string)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, address p2, bool p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,address,bool)\", p0, p1, p2, p3));\n\t}\n\n\tfunction log(address p0, address p1, address p2, address p3) internal view {\n\t\t_sendLogPayload(abi.encodeWithSignature(\"log(address,address,address,address)\", p0, p1, p2, p3));\n\t}\n\n}\n" + }, + "contracts/oracles/YearnChainlinkOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Chainlink Aggregator\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ninterface IYearnVault {\n function pricePerShare() external view returns (uint256 price);\n}\n\ncontract YearnChainlinkOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n // Calculates the lastest exchange rate\n // Uses both divide and multiply only for tokens not supported directly by Chainlink, for example MKR/USD\n function _get(\n address multiply,\n address divide,\n uint256 decimals,\n address yearnVault\n ) internal view returns (uint256) {\n uint256 price = uint256(1e36);\n if (multiply != address(0)) {\n price = price.mul(uint256(IAggregator(multiply).latestAnswer()));\n } else {\n price = price.mul(1e18);\n }\n\n if (divide != address(0)) {\n price = price / uint256(IAggregator(divide).latestAnswer());\n }\n\n // @note decimals have to take into account the decimals of the vault asset\n return price / decimals.mul(IYearnVault(yearnVault).pricePerShare());\n }\n\n function getDataParameter(\n address multiply,\n address divide,\n uint256 decimals,\n address yearnVault\n ) public pure returns (bytes memory) {\n return abi.encode(multiply, divide, decimals, yearnVault);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n (address multiply, address divide, uint256 decimals, address yearnVault) = abi.decode(data, (address, address, uint256, address));\n return (true, _get(multiply, divide, decimals, yearnVault));\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (address multiply, address divide, uint256 decimals, address yearnVault) = abi.decode(data, (address, address, uint256, address));\n return (true, _get(multiply, divide, decimals, yearnVault));\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"LINK\";\n }\n}\n" + }, + "contracts/oracles/xSUSHIOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\n/// @title xSUSHIOracle\n/// @author BoringCrypto\n/// @notice Oracle used for getting the price of xSUSHI based on Chainlink\n/// @dev\ncontract xSUSHIOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IERC20 public immutable sushi;\n IERC20 public immutable bar;\n IAggregator public immutable sushiOracle;\n\n constructor(\n IERC20 sushi_,\n IERC20 bar_,\n IAggregator sushiOracle_\n ) public {\n sushi = sushi_;\n bar = bar_;\n sushiOracle = sushiOracle_;\n }\n\n // Calculates the lastest exchange rate\n // Uses sushi rate and xSUSHI conversion and divide for any conversion other than from SUSHI to ETH\n function _get(address divide, uint256 decimals) internal view returns (uint256) {\n uint256 price = uint256(1e36);\n price = (price.mul(uint256(sushiOracle.latestAnswer())) / bar.totalSupply()).mul(sushi.balanceOf(address(bar)));\n\n if (divide != address(0)) {\n price = price / uint256(IAggregator(divide).latestAnswer());\n }\n\n return price / decimals;\n }\n\n function getDataParameter(address divide, uint256 decimals) public pure returns (bytes memory) {\n return abi.encode(divide, decimals);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n (address divide, uint256 decimals) = abi.decode(data, (address, uint256));\n return (true, _get(divide, decimals));\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (address divide, uint256 decimals) = abi.decode(data, (address, uint256));\n return (true, _get(divide, decimals));\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"xSUSHI Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"xSUSHI-LINK\";\n }\n}\n" + }, + "contracts/oracles/wOHMLinkOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Chainlink Aggregator\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ninterface IWOHM {\n function sOHMTowOHM( uint256 _amount ) external view returns ( uint256 );\n}\n\ncontract wOHMOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IAggregator public constant ohmOracle = IAggregator(0x90c2098473852E2F07678Fe1B6d595b1bd9b16Ed);\n IAggregator public constant ethUSDOracle = IAggregator(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);\n IWOHM public constant WOHM = IWOHM(0xCa76543Cf381ebBB277bE79574059e32108e3E65);\n\n // Calculates the lastest exchange rate\n // Uses both divide and multiply only for tokens not supported directly by Chainlink, for example MKR/USD\n function _get() internal view returns (uint256) {\n return 1e44 / (uint256(1e18).mul(uint256(ohmOracle.latestAnswer()).mul(uint256(ethUSDOracle.latestAnswer()))) / WOHM.sOHMTowOHM(1e9));\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata) public override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata ) public view override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"wOHM Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"LINK/wOHM\";\n }\n}\n" + }, + "contracts/oracles/wMEMOOracle.sol": { + "content": "// SPDX-License-Identifier: AGPL-3.0-only\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../libraries/FixedPoint.sol\";\n\n// solhint-disable not-rely-on-time\n\n// adapted from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ninterface IWMEMO {\n function MEMOTowMEMO( uint256 _amount ) external view returns ( uint256 );\n}\n\ncontract wMemoOracle is IOracle {\n using FixedPoint for *;\n using BoringMath for uint256;\n uint256 public constant PERIOD = 10 minutes;\n IAggregator public constant MIM_USD = IAggregator(0x54EdAB30a7134A16a54218AE64C73e1DAf48a8Fb);\n IUniswapV2Pair public constant WMEMO_MIM = IUniswapV2Pair(0x4d308C46EA9f234ea515cC51F16fba776451cac8);\n\n IWMEMO public constant WMEMO = IWMEMO(0x0da67235dD5787D67955420C84ca1cEcd4E5Bb3b);\n\n struct PairInfo {\n uint256 priceCumulativeLast;\n uint32 blockTimestampLast;\n uint144 priceAverage;\n }\n\n PairInfo public pairInfo;\n function _get(uint32 blockTimestamp) public view returns (uint256) {\n uint256 priceCumulative = WMEMO_MIM.price0CumulativeLast();\n\n // if time has elapsed since the last update on the pair, mock the accumulated price values\n (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(WMEMO_MIM).getReserves();\n priceCumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * (blockTimestamp - blockTimestampLast); // overflows ok\n\n // overflow is desired, casting never truncates\n // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed\n return priceCumulative;\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata data) external override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = _get(blockTimestamp);\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n pairInfo.priceAverage = uint144(1e44 / uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144()).mul(uint256(MIM_USD.latestAnswer())));\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = priceCumulative;\n\n return (true, pairInfo.priceAverage);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n uint144 priceAverage = uint144(1e44 / uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144()).mul(uint256(MIM_USD.latestAnswer())));\n\n return (true, priceAverage);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (uint256 reserve0, uint256 reserve1, ) = WMEMO_MIM.getReserves();\n rate = 1e44 / (reserve1.mul(1e18) / reserve0).mul(uint256(MIM_USD.latestAnswer()));\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"wMEMO TWAP\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"wMEMO\";\n }\n}\n" + }, + "contracts/oracles/SpellTWAPOracle.sol": { + "content": "// SPDX-License-Identifier: AGPL-3.0-only\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"../libraries/FixedPoint.sol\";\n\n// solhint-disable not-rely-on-time\n\n// adapted from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ncontract SpellTWAPOracle is IOracle {\n using FixedPoint for *;\n using BoringMath for uint256;\n uint256 public constant PERIOD = 10 minutes;\n IAggregator public constant ETH_USD = IAggregator(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);\n IUniswapV2Pair public constant pair = IUniswapV2Pair(0xb5De0C3753b6E1B4dBA616Db82767F17513E6d4E);\n\n IERC20 public constant SSPELL = IERC20(0x26FA3fFFB6EfE8c1E69103aCb4044C26B9A106a9);\n IERC20 public constant SPELL = IERC20(0x090185f2135308BaD17527004364eBcC2D37e5F6);\n\n struct PairInfo {\n uint256 priceCumulativeLast;\n uint32 blockTimestampLast;\n uint144 priceAverage;\n }\n\n PairInfo public pairInfo;\n function _get(uint32 blockTimestamp) public view returns (uint256) {\n uint256 priceCumulative = pair.price0CumulativeLast();\n\n // if time has elapsed since the last update on the pair, mock the accumulated price values\n (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves();\n priceCumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * (blockTimestamp - blockTimestampLast); // overflows ok\n\n // overflow is desired, casting never truncates\n // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed\n return priceCumulative;\n }\n\n function toSSpell(uint256 amount) internal view returns (uint256) {\n return amount.mul(SPELL.balanceOf(address(SSPELL))) / SSPELL.totalSupply();\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata data) external override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = _get(blockTimestamp);\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n pairInfo.priceAverage = uint144(1e44 / toSSpell(uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144())).mul(uint256(ETH_USD.latestAnswer())));\n pairInfo.blockTimestampLast = blockTimestamp;\n pairInfo.priceCumulativeLast = priceCumulative;\n\n return (true, pairInfo.priceAverage);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairInfo.blockTimestampLast == 0) {\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairInfo.blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairInfo.priceAverage);\n }\n\n uint256 priceCumulative = _get(blockTimestamp);\n uint144 priceAverage = uint144(1e44 / toSSpell(uint256(FixedPoint\n .uq112x112(uint224((priceCumulative - pairInfo.priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144())).mul(uint256(ETH_USD.latestAnswer())));\n\n return (true, priceAverage);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n rate = 1e44 / toSSpell(reserve1.mul(1e18) / reserve0).mul(uint256(ETH_USD.latestAnswer()));\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"SSpell TWAP CHAINLINK\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"SSpell\";\n }\n}\n" + }, + "contracts/oracles/SimpleSLPTWAP1Oracle.sol": { + "content": "// SPDX-License-Identifier: AGPL-3.0-only\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../libraries/FixedPoint.sol\";\n\n// solhint-disable not-rely-on-time\n\n// adapted from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol\n\ncontract SimpleSLPTWAP1Oracle is IOracle {\n using FixedPoint for *;\n using BoringMath for uint256;\n uint256 public constant PERIOD = 5 minutes;\n\n struct PairInfo {\n uint256 priceCumulativeLast;\n uint32 blockTimestampLast;\n uint144 priceAverage;\n }\n\n mapping(IUniswapV2Pair => PairInfo) public pairs; // Map of pairs and their info\n mapping(address => IUniswapV2Pair) public callerInfo; // Map of callers to pairs\n\n function _get(IUniswapV2Pair pair, uint32 blockTimestamp) public view returns (uint256) {\n uint256 priceCumulative = pair.price1CumulativeLast();\n\n // if time has elapsed since the last update on the pair, mock the accumulated price values\n (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves();\n priceCumulative += uint256(FixedPoint.fraction(reserve0, reserve1)._x) * (blockTimestamp - blockTimestampLast); // overflows ok\n\n // overflow is desired, casting never truncates\n // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed\n return priceCumulative;\n }\n\n function getDataParameter(IUniswapV2Pair pair) public pure returns (bytes memory) {\n return abi.encode(pair);\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata data) external override returns (bool, uint256) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairs[pair].blockTimestampLast == 0) {\n pairs[pair].blockTimestampLast = blockTimestamp;\n pairs[pair].priceCumulativeLast = _get(pair, blockTimestamp);\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairs[pair].blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairs[pair].priceAverage);\n }\n\n uint256 priceCumulative = _get(pair, blockTimestamp);\n pairs[pair].priceAverage = FixedPoint\n .uq112x112(uint224((priceCumulative - pairs[pair].priceCumulativeLast) / timeElapsed))\n .mul(10**18)\n .decode144();\n pairs[pair].blockTimestampLast = blockTimestamp;\n pairs[pair].priceCumulativeLast = priceCumulative;\n\n return (true, pairs[pair].priceAverage);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairs[pair].blockTimestampLast == 0) {\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairs[pair].blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairs[pair].priceAverage);\n }\n\n uint256 priceCumulative = _get(pair, blockTimestamp);\n uint144 priceAverage =\n FixedPoint.uq112x112(uint224((priceCumulative - pairs[pair].priceCumulativeLast) / timeElapsed)).mul(10**18).decode144();\n\n return (true, priceAverage);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n rate = reserve0.mul(1e18) / reserve1;\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"SushiSwap TWAP\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"S\";\n }\n}\n" + }, + "contracts/oracles/SimpleSLPTWAP0Oracle.sol": { + "content": "// SPDX-License-Identifier: AGPL-3.0-only\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../libraries/FixedPoint.sol\";\n\n// solhint-disable not-rely-on-time\n\n// adapted from https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol\n\ncontract SimpleSLPTWAP0Oracle is IOracle {\n using FixedPoint for *;\n using BoringMath for uint256;\n uint256 public constant PERIOD = 5 minutes;\n\n struct PairInfo {\n uint256 priceCumulativeLast;\n uint32 blockTimestampLast;\n uint144 priceAverage;\n }\n\n mapping(IUniswapV2Pair => PairInfo) public pairs; // Map of pairs and their info\n mapping(address => IUniswapV2Pair) public callerInfo; // Map of callers to pairs\n\n function _get(IUniswapV2Pair pair, uint32 blockTimestamp) public view returns (uint256) {\n uint256 priceCumulative = pair.price0CumulativeLast();\n\n // if time has elapsed since the last update on the pair, mock the accumulated price values\n (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves();\n priceCumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * (blockTimestamp - blockTimestampLast); // overflows ok\n\n // overflow is desired, casting never truncates\n // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed\n return priceCumulative;\n }\n\n function getDataParameter(IUniswapV2Pair pair) public pure returns (bytes memory) {\n return abi.encode(pair);\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata data) external override returns (bool, uint256) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairs[pair].blockTimestampLast == 0) {\n pairs[pair].blockTimestampLast = blockTimestamp;\n pairs[pair].priceCumulativeLast = _get(pair, blockTimestamp);\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairs[pair].blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairs[pair].priceAverage);\n }\n\n uint256 priceCumulative = _get(pair, blockTimestamp);\n pairs[pair].priceAverage = FixedPoint\n .uq112x112(uint224((priceCumulative - pairs[pair].priceCumulativeLast) / timeElapsed))\n .mul(1e18)\n .decode144();\n pairs[pair].blockTimestampLast = blockTimestamp;\n pairs[pair].priceCumulativeLast = priceCumulative;\n\n return (true, pairs[pair].priceAverage);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n uint32 blockTimestamp = uint32(block.timestamp);\n if (pairs[pair].blockTimestampLast == 0) {\n return (false, 0);\n }\n uint32 timeElapsed = blockTimestamp - pairs[pair].blockTimestampLast; // overflow is desired\n if (timeElapsed < PERIOD) {\n return (true, pairs[pair].priceAverage);\n }\n\n uint256 priceCumulative = _get(pair, blockTimestamp);\n uint144 priceAverage =\n FixedPoint.uq112x112(uint224((priceCumulative - pairs[pair].priceCumulativeLast) / timeElapsed)).mul(1e18).decode144();\n\n return (true, priceAverage);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n IUniswapV2Pair pair = abi.decode(data, (IUniswapV2Pair));\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n rate = reserve1.mul(1e18) / reserve0;\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"SushiSwap TWAP\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"S\";\n }\n}\n" + }, + "contracts/oracles/ProxyOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\n/// @title ProxyOracle\n/// @author 0xMerlin\n/// @notice Oracle used for getting the price of xSUSHI based on Chainlink\ncontract ProxyOracle is IOracle, BoringOwnable {\n\n IOracle public oracleImplementation;\n\n event LogOracleImplementationChange(IOracle indexed oldOracle, IOracle indexed newOracle);\n\n constructor() public {\n }\n\n function changeOracleImplementation(IOracle newOracle) external onlyOwner {\n IOracle oldOracle = oracleImplementation;\n oracleImplementation = newOracle;\n emit LogOracleImplementationChange(oldOracle, newOracle);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n return oracleImplementation.get(data);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n return oracleImplementation.peek(data);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n return oracleImplementation.peekSpot(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"Proxy Oracle\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"Proxy\";\n }\n}\n" + }, + "contracts/oracles/PeggedOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"../interfaces/IOracle.sol\";\n\n/// @title PeggedOracle\n/// @author BoringCrypto\n/// @notice Oracle used for pegged prices that don't change\n/// @dev\ncontract PeggedOracle is IOracle {\n /// @notice\n /// @dev\n /// @param rate (uint256) The fixed exchange rate\n /// @return (bytes)\n function getDataParameter(uint256 rate) public pure returns (bytes memory) {\n return abi.encode(rate);\n }\n\n // Get the exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n uint256 rate = abi.decode(data, (uint256));\n return (rate != 0, rate);\n }\n\n // Check the exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n uint256 rate = abi.decode(data, (uint256));\n return (rate != 0, rate);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"Pegged\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"PEG\";\n }\n}\n" + }, + "contracts/oracles/dQuickOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"../interfaces/IOracle.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\n/// @title xSUSHIOracle\n/// @author BoringCrypto\n/// @notice Oracle used for getting the price of xSUSHI based on Chainlink\n/// @dev\ncontract xSUSHIOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IERC20 public immutable sushi;\n IERC20 public immutable bar;\n IAggregator public immutable sushiOracle;\n\n constructor(\n IERC20 sushi_,\n IERC20 bar_,\n IAggregator sushiOracle_\n ) public {\n sushi = sushi_;\n bar = bar_;\n sushiOracle = sushiOracle_;\n }\n\n // Calculates the lastest exchange rate\n // Uses sushi rate and xSUSHI conversion and divide for any conversion other than from SUSHI to ETH\n function _get(address divide, uint256 decimals) internal view returns (uint256) {\n uint256 price = uint256(1e36);\n price = (price.mul(uint256(sushiOracle.latestAnswer())) / bar.totalSupply()).mul(sushi.balanceOf(address(bar)));\n\n if (divide != address(0)) {\n price = price / uint256(IAggregator(divide).latestAnswer());\n }\n\n return price / decimals;\n }\n\n function getDataParameter(address divide, uint256 decimals) public pure returns (bytes memory) {\n return abi.encode(divide, decimals);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n (address divide, uint256 decimals) = abi.decode(data, (address, uint256));\n return (true, _get(divide, decimals));\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (address divide, uint256 decimals) = abi.decode(data, (address, uint256));\n return (true, _get(divide, decimals));\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"xSUSHI Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"xSUSHI-LINK\";\n }\n}\n" + }, + "contracts/oracles/CompoundOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\ninterface IUniswapAnchoredView {\n function price(string memory symbol) external view returns (uint256);\n}\n\ncontract CompoundOracle is IOracle {\n using BoringMath for uint256;\n\n IUniswapAnchoredView private constant ORACLE = IUniswapAnchoredView(0x922018674c12a7F0D394ebEEf9B58F186CdE13c1);\n\n struct PriceInfo {\n uint128 price;\n uint128 blockNumber;\n }\n\n mapping(string => PriceInfo) public prices;\n\n function _peekPrice(string memory symbol) internal view returns (uint256) {\n if (bytes(symbol).length == 0) {\n return 1000000;\n } // To allow only using collateralSymbol or assetSymbol if paired against USDx\n PriceInfo memory info = prices[symbol];\n if (block.number > info.blockNumber + 8) {\n return uint128(ORACLE.price(symbol)); // Prices are denominated with 6 decimals, so will fit in uint128\n }\n return info.price;\n }\n\n function _getPrice(string memory symbol) internal returns (uint256) {\n if (bytes(symbol).length == 0) {\n return 1000000;\n } // To allow only using collateralSymbol or assetSymbol if paired against USDx\n PriceInfo memory info = prices[symbol];\n if (block.number > info.blockNumber + 8) {\n info.price = uint128(ORACLE.price(symbol)); // Prices are denominated with 6 decimals, so will fit in uint128\n info.blockNumber = uint128(block.number); // Blocknumber will fit in uint128\n prices[symbol] = info;\n }\n return info.price;\n }\n\n function getDataParameter(\n string memory collateralSymbol,\n string memory assetSymbol,\n uint256 division\n ) public pure returns (bytes memory) {\n return abi.encode(collateralSymbol, assetSymbol, division);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n return (true, uint256(1e36).mul(_getPrice(assetSymbol)) / _getPrice(collateralSymbol) / division);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256));\n return (true, uint256(1e36).mul(_peekPrice(assetSymbol)) / _peekPrice(collateralSymbol) / division);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"Compound\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"COMP\";\n }\n}\n" + }, + "contracts/oracles/CompositeOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Using the same Copyleft License as in the original Repository\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\ncontract CompositeOracle is IOracle {\n using BoringMath for uint256;\n\n function getDataParameter(\n IOracle oracle1,\n IOracle oracle2,\n bytes memory data1,\n bytes memory data2\n ) public pure returns (bytes memory) {\n return abi.encode(oracle1, oracle2, data1, data2);\n }\n\n // Get the latest exchange rate, if no valid (recent) rate is available, return false\n /// @inheritdoc IOracle\n function get(bytes calldata data) external override returns (bool, uint256) {\n (IOracle oracle1, IOracle oracle2, bytes memory data1, bytes memory data2) = abi.decode(data, (IOracle, IOracle, bytes, bytes));\n (bool success1, uint256 price1) = oracle1.get(data1);\n (bool success2, uint256 price2) = oracle2.get(data2);\n return (success1 && success2, price1.mul(price2) / 10**18);\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (IOracle oracle1, IOracle oracle2, bytes memory data1, bytes memory data2) = abi.decode(data, (IOracle, IOracle, bytes, bytes));\n (bool success1, uint256 price1) = oracle1.peek(data1);\n (bool success2, uint256 price2) = oracle2.peek(data2);\n return (success1 && success2, price1.mul(price2) / 10**18);\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (IOracle oracle1, IOracle oracle2, bytes memory data1, bytes memory data2) = abi.decode(data, (IOracle, IOracle, bytes, bytes));\n uint256 price1 = oracle1.peekSpot(data1);\n uint256 price2 = oracle2.peekSpot(data2);\n return price1.mul(price2) / 10**18;\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata data) public view override returns (string memory) {\n (IOracle oracle1, IOracle oracle2, bytes memory data1, bytes memory data2) = abi.decode(data, (IOracle, IOracle, bytes, bytes));\n return string(abi.encodePacked(oracle1.name(data1), \"+\", oracle2.name(data2)));\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata data) public view override returns (string memory) {\n (IOracle oracle1, IOracle oracle2, bytes memory data1, bytes memory data2) = abi.decode(data, (IOracle, IOracle, bytes, bytes));\n return string(abi.encodePacked(oracle1.symbol(data1), \"+\", oracle2.symbol(data2)));\n }\n}\n" + }, + "contracts/oracles/ChainlinkOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Chainlink Aggregator\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ncontract ChainlinkOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n // Calculates the lastest exchange rate\n // Uses both divide and multiply only for tokens not supported directly by Chainlink, for example MKR/USD\n function _get(\n address multiply,\n address divide,\n uint256 decimals\n ) internal view returns (uint256) {\n uint256 price = uint256(1e36);\n if (multiply != address(0)) {\n price = price.mul(uint256(IAggregator(multiply).latestAnswer()));\n } else {\n price = price.mul(1e18);\n }\n\n if (divide != address(0)) {\n price = price / uint256(IAggregator(divide).latestAnswer());\n }\n\n return price / decimals;\n }\n\n function getDataParameter(\n address multiply,\n address divide,\n uint256 decimals\n ) public pure returns (bytes memory) {\n return abi.encode(multiply, divide, decimals);\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata data) public override returns (bool, uint256) {\n (address multiply, address divide, uint256 decimals) = abi.decode(data, (address, address, uint256));\n return (true, _get(multiply, divide, decimals));\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata data) public view override returns (bool, uint256) {\n (address multiply, address divide, uint256 decimals) = abi.decode(data, (address, address, uint256));\n return (true, _get(multiply, divide, decimals));\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"LINK\";\n }\n}\n" + }, + "contracts/oracles/BandOracleFTM.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma experimental ABIEncoderV2;\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Band\n\ninterface IStdReference {\n /// A structure returned whenever someone requests for standard reference data.\n struct ReferenceData {\n uint256 rate; // base/quote exchange rate, multiplied by 1e18.\n uint256 lastUpdatedBase; // UNIX epoch of the last time when base price gets updated.\n uint256 lastUpdatedQuote; // UNIX epoch of the last time when quote price gets updated.\n }\n\n /// Returns the price data for the given base/quote pair. Revert if not available.\n function getReferenceData(string memory _base, string memory _quote)\n external\n view\n returns (ReferenceData memory);\n\n /// Similar to getReferenceData, but with multiple base/quote pairs at once.\n function getReferenceDataBulk(string[] memory _bases, string[] memory _quotes)\n external\n view\n returns (ReferenceData[] memory);\n}\n\ncontract BandOracleFTMV1 is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IStdReference constant ftmOracle = IStdReference(0x56E2898E0ceFF0D1222827759B56B28Ad812f92F);\n\n // Calculates the lastest exchange rate\n function _get() internal view returns (uint256 rate) {\n IStdReference.ReferenceData memory referenceData = ftmOracle.getReferenceData(\"USD\", \"FTM\");\n return referenceData.rate;\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata) public override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata) public view override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"BAND FTM/USD\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"BAND\";\n }\n}" + }, + "contracts/oracles/AVAXOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Chainlink Aggregator\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ncontract AVAXOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IAggregator public constant avaxOracle = IAggregator(0x0A77230d17318075983913bC2145DB16C7366156);\n\n // Calculates the lastest exchange rate\n // Uses both divide and multiply only for tokens not supported directly by Chainlink, for example MKR/USD\n function _get() internal view returns (uint256) {\n return 1e26 / uint256(avaxOracle.latestAnswer());\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata) public override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata ) public view override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"AVAX Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"LINK/AVAX\";\n }\n}\n" + }, + "contracts/oracles/ALCXOracle.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// Chainlink Aggregator\n\ninterface IAggregator {\n function latestAnswer() external view returns (int256 answer);\n}\n\ncontract ALCXOracle is IOracle {\n using BoringMath for uint256; // Keep everything in uint256\n\n IAggregator public constant alcxOracle = IAggregator(0x194a9AaF2e0b67c35915cD01101585A33Fe25CAa);\n IAggregator public constant ethUSDOracle = IAggregator(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);\n\n // Calculates the lastest exchange rate\n // Uses both divide and multiply only for tokens not supported directly by Chainlink, for example MKR/USD\n function _get() internal view returns (uint256) {\n return 1e44 / uint256(alcxOracle.latestAnswer()).mul(uint256(ethUSDOracle.latestAnswer()));\n }\n\n // Get the latest exchange rate\n /// @inheritdoc IOracle\n function get(bytes calldata) public override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the last exchange rate without any state changes\n /// @inheritdoc IOracle\n function peek(bytes calldata ) public view override returns (bool, uint256) {\n return (true, _get());\n }\n\n // Check the current spot exchange rate without any state changes\n /// @inheritdoc IOracle\n function peekSpot(bytes calldata data) external view override returns (uint256 rate) {\n (, rate) = peek(data);\n }\n\n /// @inheritdoc IOracle\n function name(bytes calldata) public view override returns (string memory) {\n return \"ALCX Chainlink\";\n }\n\n /// @inheritdoc IOracle\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"LINK/ALCX\";\n }\n}\n" + }, + "contracts/mocks/OracleMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../interfaces/IOracle.sol\";\n\n// WARNING: This oracle is only for testing, please use PeggedOracle for a fixed value oracle\ncontract OracleMock is IOracle {\n using BoringMath for uint256;\n\n uint256 public rate;\n bool public success;\n\n constructor() public {\n success = true;\n }\n\n function set(uint256 rate_) public {\n // The rate can be updated.\n rate = rate_;\n }\n\n function setSuccess(bool val) public {\n success = val;\n }\n\n function getDataParameter() public pure returns (bytes memory) {\n return abi.encode(\"0x0\");\n }\n\n // Get the latest exchange rate\n function get(bytes calldata) public override returns (bool, uint256) {\n return (success, rate);\n }\n\n // Check the last exchange rate without any state changes\n function peek(bytes calldata) public view override returns (bool, uint256) {\n return (success, rate);\n }\n\n function peekSpot(bytes calldata) public view override returns (uint256) {\n return rate;\n }\n\n function name(bytes calldata) public view override returns (string memory) {\n return \"Test\";\n }\n\n function symbol(bytes calldata) public view override returns (string memory) {\n return \"TEST\";\n }\n}\n" + }, + "contracts/interfaces/IKashiPair.sol": { + "content": "// SPDX-License-Identifier: MIT\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./IOracle.sol\";\nimport \"./ISwapper.sol\";\n\ninterface IKashiPair {\n event Approval(address indexed _owner, address indexed _spender, uint256 _value);\n event LogAccrue(uint256 accruedAmount, uint256 feeFraction, uint64 rate, uint256 utilization);\n event LogAddAsset(address indexed from, address indexed to, uint256 share, uint256 fraction);\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogExchangeRate(uint256 rate);\n event LogFeeTo(address indexed newFeeTo);\n event LogRemoveAsset(address indexed from, address indexed to, uint256 share, uint256 fraction);\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n event Transfer(address indexed _from, address indexed _to, uint256 _value);\n\n function DOMAIN_SEPARATOR() external view returns (bytes32);\n\n function accrue() external;\n\n function accrueInfo()\n external\n view\n returns (\n uint64 interestPerBlock,\n uint64 lastBlockAccrued,\n uint128 feesEarnedFraction\n );\n\n function addAsset(\n address to,\n bool skim,\n uint256 share\n ) external returns (uint256 fraction);\n\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) external;\n\n function allowance(address, address) external view returns (uint256);\n\n function approve(address spender, uint256 amount) external returns (bool);\n\n function asset() external view returns (IERC20);\n\n function balanceOf(address) external view returns (uint256);\n\n function bentoBox() external view returns (IBentoBoxV1);\n\n function borrow(address to, uint256 amount) external returns (uint256 part, uint256 share);\n\n function claimOwnership() external;\n\n function collateral() external view returns (IERC20);\n\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2);\n\n function decimals() external view returns (uint8);\n\n function exchangeRate() external view returns (uint256);\n\n function feeTo() external view returns (address);\n\n function getInitData(\n IERC20 collateral_,\n IERC20 asset_,\n IOracle oracle_,\n bytes calldata oracleData_\n ) external pure returns (bytes memory data);\n\n function init(bytes calldata data) external payable;\n\n function isSolvent(address user, bool open) external view returns (bool);\n\n function liquidate(\n address[] calldata users,\n uint256[] calldata borrowParts,\n address to,\n ISwapper swapper,\n bool open\n ) external;\n\n function masterContract() external view returns (address);\n\n function name() external view returns (string memory);\n\n function nonces(address) external view returns (uint256);\n\n function oracle() external view returns (IOracle);\n\n function oracleData() external view returns (bytes memory);\n\n function owner() external view returns (address);\n\n function pendingOwner() external view returns (address);\n\n function permit(\n address owner_,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external;\n\n function removeAsset(address to, uint256 fraction) external returns (uint256 share);\n\n function removeCollateral(address to, uint256 share) external;\n\n function repay(\n address to,\n bool skim,\n uint256 part\n ) external returns (uint256 amount);\n\n function setFeeTo(address newFeeTo) external;\n\n function setSwapper(ISwapper swapper, bool enable) external;\n\n function swappers(ISwapper) external view returns (bool);\n\n function symbol() external view returns (string memory);\n\n function totalAsset() external view returns (uint128 elastic, uint128 base);\n\n function totalBorrow() external view returns (uint128 elastic, uint128 base);\n\n function totalCollateralShare() external view returns (uint256);\n\n function totalSupply() external view returns (uint256);\n\n function transfer(address to, uint256 amount) external returns (bool);\n\n function transferFrom(\n address from,\n address to,\n uint256 amount\n ) external returns (bool);\n\n function transferOwnership(\n address newOwner,\n bool direct,\n bool renounce\n ) external;\n\n function updateExchangeRate() external returns (bool updated, uint256 rate);\n\n function userBorrowPart(address) external view returns (uint256);\n\n function userCollateralShare(address) external view returns (uint256);\n\n function withdrawFees() external;\n}\n" + }, + "contracts/CauldronV2MultiChain.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract CauldronV2MultiChain is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n CauldronV2MultiChain public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n uint64 INTEREST_PER_SECOND;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 public COLLATERIZATION_RATE;\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 public LIQUIDATION_MULTIPLIER; \r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 public BORROW_OPENING_FEE;\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n uint256 private constant DISTRIBUTION_PART = 10;\r\n uint256 private constant DISTRIBUTION_PRECISION = 100;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n // Apply a percentual fee share to sSpell holders\r\n \r\n {\r\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\r\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\r\n }\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), masterContract.owner(), amount, 0);\r\n }\r\n}\r\n" + }, + "contracts/CauldronV2Checkpoint.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Cauldron\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./MagicInternetMoney.sol\";\nimport \"./interfaces/IOracle.sol\";\nimport \"./interfaces/ISwapper.sol\";\nimport \"./interfaces/ICheckpointToken.sol\";\n\n// solhint-disable avoid-low-level-calls\n// solhint-disable no-inline-assembly\n\n/// @title Cauldron\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract CauldronV2Checkpoint is BoringOwnable, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogExchangeRate(uint256 rate);\n event LogAccrue(uint128 accruedAmount);\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n CauldronV2Checkpoint public immutable masterContract;\n IERC20 public immutable magicInternetMoney;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC20 public collateral;\n IOracle public oracle;\n bytes public oracleData;\n\n // Total amounts\n uint256 public totalCollateralShare; // Total collateral supplied\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\n\n // User balances\n mapping(address => uint256) public userCollateralShare;\n mapping(address => uint256) public userBorrowPart;\n\n /// @notice Exchange and interest rate tracking.\n /// This is 'cached' here because calls to Oracles can be very expensive.\n uint256 public exchangeRate;\n\n struct AccrueInfo {\n uint64 lastAccrued;\n uint128 feesEarned;\n uint64 INTEREST_PER_SECOND;\n }\n\n AccrueInfo public accrueInfo;\n\n // Settings\n uint256 public COLLATERIZATION_RATE;\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\n\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\n\n uint256 public LIQUIDATION_MULTIPLIER; \n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\n\n uint256 public BORROW_OPENING_FEE;\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\n\n uint256 private constant DISTRIBUTION_PART = 10;\n uint256 private constant DISTRIBUTION_PRECISION = 100;\n\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\n bentoBox = bentoBox_;\n magicInternetMoney = magicInternetMoney_;\n masterContract = this;\n }\n\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\n function init(bytes calldata data) public payable override {\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\n }\n\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\n function accrue() public {\n AccrueInfo memory _accrueInfo = accrueInfo;\n // Number of seconds since accrue was called\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\n if (elapsedTime == 0) {\n return;\n }\n _accrueInfo.lastAccrued = uint64(block.timestamp);\n\n Rebase memory _totalBorrow = totalBorrow;\n if (_totalBorrow.base == 0) {\n accrueInfo = _accrueInfo;\n return;\n }\n\n // Accrue interest\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\n\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\n totalBorrow = _totalBorrow;\n accrueInfo = _accrueInfo;\n\n emit LogAccrue(extraAmount);\n }\n\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\n // accrue must have already been called!\n uint256 borrowPart = userBorrowPart[user];\n if (borrowPart == 0) return true;\n uint256 collateralShare = userCollateralShare[user];\n if (collateralShare == 0) return false;\n\n Rebase memory _totalBorrow = totalBorrow;\n\n return\n bentoBox.toAmount(\n collateral,\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\n false\n ) >=\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\n }\n\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\n modifier solvent() {\n _;\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\n }\n\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\n /// @return updated True if `exchangeRate` was updated.\n /// @return rate The new exchange rate.\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\n (updated, rate) = oracle.get(oracleData);\n\n if (updated) {\n exchangeRate = rate;\n emit LogExchangeRate(rate);\n } else {\n // Return the old rate if fetching wasn't successful\n rate = exchangeRate;\n }\n }\n\n /// @dev Helper function to move tokens.\n /// @param token The ERC-20 token.\n /// @param share The amount in shares to add.\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\n /// Only used for accounting checks.\n /// @param skim If True, only does a balance check on this contract.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n function _addTokens(\n IERC20 token,\n uint256 share,\n uint256 total,\n bool skim\n ) internal {\n if (skim) {\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\n } else {\n bentoBox.transfer(token, msg.sender, address(this), share);\n }\n }\n\n /// @notice Adds `collateral` from msg.sender to the account `to`.\n /// @param to The receiver of the tokens.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param share The amount of shares to add for `to`.\n function addCollateral(\n address to,\n bool skim,\n uint256 share\n ) public {\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([to,address(0)]);\n\n userCollateralShare[to] = userCollateralShare[to].add(share);\n uint256 oldTotalCollateralShare = totalCollateralShare;\n totalCollateralShare = oldTotalCollateralShare.add(share);\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\n }\n\n /// @dev Concrete implementation of `removeCollateral`.\n function _removeCollateral(address to, uint256 share) internal {\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([address(msg.sender),address(0)]);\n\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\n totalCollateralShare = totalCollateralShare.sub(share);\n emit LogRemoveCollateral(msg.sender, to, share);\n bentoBox.transfer(collateral, address(this), to, share);\n }\n\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\n /// @param to The receiver of the shares.\n /// @param share Amount of shares to remove.\n function removeCollateral(address to, uint256 share) public solvent {\n // accrue must be called because we check solvency\n accrue();\n _removeCollateral(to, share);\n }\n\n /// @dev Concrete implementation of `borrow`.\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\n\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\n share = bentoBox.toShare(magicInternetMoney, amount, false);\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\n\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\n }\n\n /// @notice Sender borrows `amount` and transfers it to `to`.\n /// @return part Total part of the debt held by borrowers.\n /// @return share Total amount in shares borrowed.\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\n accrue();\n (part, share) = _borrow(to, amount);\n }\n\n /// @dev Concrete implementation of `repay`.\n function _repay(\n address to,\n bool skim,\n uint256 part\n ) internal returns (uint256 amount) {\n (totalBorrow, amount) = totalBorrow.sub(part, true);\n userBorrowPart[to] = userBorrowPart[to].sub(part);\n\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\n }\n\n /// @notice Repays a loan.\n /// @param to Address of the user this payment should go.\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\n /// @param part The amount to repay. See `userBorrowPart`.\n /// @return amount The total amount repayed.\n function repay(\n address to,\n bool skim,\n uint256 part\n ) public returns (uint256 amount) {\n accrue();\n amount = _repay(to, skim, part);\n }\n\n // Functions that need accrue to be called\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n uint8 internal constant ACTION_BORROW = 5;\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\n uint8 internal constant ACTION_ACCRUE = 8;\n\n // Functions that don't need accrue to be called\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\n abi.decode(data, (address, bytes, bool, bool, uint8));\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\n\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\n require(success, \"Cauldron: call failed\");\n return (returnData, returnValues);\n }\n\n struct CookStatus {\n bool needsSolvencyCheck;\n bool hasAccrued;\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n CookStatus memory status;\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (!status.hasAccrued && action < 10) {\n accrue();\n status.hasAccrued = true;\n }\n if (action == ACTION_ADD_COLLATERAL) {\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n addCollateral(to, skim, _num(share, value1, value2));\n } else if (action == ACTION_REPAY) {\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\n _repay(to, skim, _num(part, value1, value2));\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\n _removeCollateral(to, _num(share, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_BORROW) {\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\n status.needsSolvencyCheck = true;\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\n (bool updated, uint256 rate) = updateExchangeRate();\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\n }\n } else if (action == ACTION_GET_REPAY_SHARE) {\n int256 part = abi.decode(datas[i], (int256));\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\n } else if (action == ACTION_GET_REPAY_PART) {\n int256 amount = abi.decode(datas[i], (int256));\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\n }\n }\n\n if (status.needsSolvencyCheck) {\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\n }\n }\n\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\n /// @param users An array of user addresses.\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\n function liquidate(\n address[] calldata users,\n uint256[] calldata maxBorrowParts,\n address to,\n ISwapper swapper\n ) public {\n // Oracle can fail but we still need to allow liquidations\n (, uint256 _exchangeRate) = updateExchangeRate();\n accrue();\n\n uint256 allCollateralShare;\n uint256 allBorrowAmount;\n uint256 allBorrowPart;\n Rebase memory _totalBorrow = totalBorrow;\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\n for (uint256 i = 0; i < users.length; i++) {\n address user = users[i];\n if (!_isSolvent(user, _exchangeRate)) {\n uint256 borrowPart;\n {\n uint256 availableBorrowPart = userBorrowPart[user];\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\n }\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\n uint256 collateralShare =\n bentoBoxTotals.toBase(\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\n false\n );\n\n //checkpoint before userCollateralShare is changed\n ICheckpointToken(address(collateral)).user_checkpoint([user,address(0)]);\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\n emit LogRemoveCollateral(user, to, collateralShare);\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\n\n // Keep totals\n allCollateralShare = allCollateralShare.add(collateralShare);\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\n allBorrowPart = allBorrowPart.add(borrowPart);\n }\n }\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\n totalBorrow = _totalBorrow;\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\n\n // Apply a percentual fee share to sSpell holders\n \n {\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\n }\n\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\n\n // Swap using a swapper freely chosen by the caller\n // Open (flash) liquidation: get proceeds first and provide the borrow after\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\n if (swapper != ISwapper(0)) {\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\n }\n\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n accrue();\n address _feeTo = masterContract.feeTo();\n uint256 _feesEarned = accrueInfo.feesEarned;\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\n accrueInfo.feesEarned = 0;\n\n emit LogWithdrawFees(_feeTo, _feesEarned);\n }\n\n /// @notice Sets the beneficiary of interest accrued.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n\n /// @notice reduces the supply of MIM\n /// @param amount amount to reduce supply by\n function reduceSupply(uint256 amount) public {\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\n }\n}\n" + }, + "contracts/interfaces/ICheckpointToken.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface ICheckpointToken {\n /// @notice checkpoint rewards for given accounts. needs to be called before any balance change\n function user_checkpoint(address[2] calldata _accounts) external returns(bool);\n}\n" + }, + "contracts/NFTPair.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\n\n// Private Pool (NFT collateral)\n\n// ( ( (\n// )\\ ) ( )\\ )\\ ) (\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\n\n// Copyright (c) 2021 BoringCrypto - All rights reserved\n// Twitter: @Boring_Crypto\n\n// Special thanks to:\n// @0xKeno - for all his invaluable contributions\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/Domain.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"./interfaces/IERC721.sol\";\n\nstruct TokenLoanParams {\n uint128 valuation; // How much will you get? OK to owe until expiration.\n uint64 duration; // Length of loan in seconds\n uint16 annualInterestBPS; // Variable cost of taking out the loan\n}\n\ninterface ILendingClub {\n // Per token settings.\n function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool);\n\n function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory);\n}\n\ninterface INFTPair {\n function collateral() external view returns (IERC721);\n\n function asset() external view returns (IERC20);\n\n function masterContract() external view returns (address);\n\n function bentoBox() external view returns (IBentoBoxV1);\n\n function removeCollateral(uint256 tokenId, address to) external;\n}\n\n/// @title NFTPair\n/// @dev This contract allows contract calls to any contract (except BentoBox)\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\ncontract NFTPair is BoringOwnable, Domain, IMasterContract {\n using BoringMath for uint256;\n using BoringMath128 for uint128;\n using RebaseLibrary for Rebase;\n using BoringERC20 for IERC20;\n\n event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS);\n event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS);\n // This automatically clears the associated loan, if any\n event LogRemoveCollateral(uint256 indexed tokenId, address recipient);\n // Details are in the loan request\n event LogLend(address indexed lender, uint256 indexed tokenId);\n event LogRepay(address indexed from, uint256 indexed tokenId);\n event LogFeeTo(address indexed newFeeTo);\n event LogWithdrawFees(address indexed feeTo, uint256 feeShare);\n\n // Immutables (for MasterContract and all clones)\n IBentoBoxV1 public immutable bentoBox;\n NFTPair public immutable masterContract;\n\n // MasterContract variables\n address public feeTo;\n\n // Per clone variables\n // Clone init settings\n IERC721 public collateral;\n IERC20 public asset;\n\n // A note on terminology:\n // \"Shares\" are BentoBox shares.\n\n // Track assets we own. Used to allow skimming the excesss.\n uint256 public feesEarnedShare;\n\n // Per token settings.\n mapping(uint256 => TokenLoanParams) public tokenLoanParams;\n\n uint8 private constant LOAN_INITIAL = 0;\n uint8 private constant LOAN_REQUESTED = 1;\n uint8 private constant LOAN_OUTSTANDING = 2;\n struct TokenLoan {\n address borrower;\n address lender;\n uint64 startTime;\n uint8 status;\n }\n mapping(uint256 => TokenLoan) public tokenLoan;\n\n // Do not go over 100% on either of these..\n uint256 private constant PROTOCOL_FEE_BPS = 1000;\n uint256 private constant OPEN_FEE_BPS = 100;\n uint256 private constant BPS = 10_000;\n uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000;\n\n // Highest order term in the Maclaurin series for exp used by\n // `calculateIntest`.\n // Intuitive interpretation: interest continuously accrues on the principal.\n // That interest, in turn, earns \"second-order\" interest-on-interest, which\n // itself earns \"third-order\" interest, etc. This constant determines how\n // far we take this until we stop counting.\n //\n // The error, in terms of the interest rate, is at least\n //\n // ----- n ----- Infinity\n // \\ x^k \\ x^k\n // e^x - ) --- , which is ) --- ,\n // / k! / k!\n // ----- k = 1 k ----- k = n + 1\n //\n // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of\n // interest that is owed at rate r over time t. It makes no difference if\n // this is, say, 5%/year for 10 years, or 50% in one year; the calculation\n // is the same. Why \"at least\"? There are also rounding errors. See\n // `calculateInterest` for more detail.\n // The factorial in the denominator \"wins\"; for all reasonable (and quite\n // a few unreasonable) interest rates, the lower-order terms contribute the\n // most to the total. The following table lists some of the calculated\n // approximations for different values of n, along with the \"true\" result:\n //\n // Total: 10% 20% 50% 100% 200% 500% 1000%\n // -----------------------------------------------------------------------\n // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0%\n // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0%\n // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7%\n // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3%\n // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7%\n // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6%\n // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3%\n // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1%\n // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3%\n // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5%\n //\n // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6%\n //\n // For instance, calculating the compounding effects of 200% in \"total\"\n // interest to the sixth order results in 635.6%, whereas the true result\n // is 638.9%.\n // At 500% that difference is a little more dramatic, but it is still in\n // the same ballpark -- and of little practical consequence unless the\n // collateral can be expected to go up more than 112 times in value.\n // Still, for volatile tokens, or an asset that is somehow known to be very\n // inflationary, use a different number.\n // Zero (no interest at all) is ignored and treated as one (linear only).\n uint8 private constant COMPOUND_INTEREST_TERMS = 6;\n\n // For signed lend / borrow requests:\n mapping(address => uint256) public nonces;\n\n /// @notice The constructor is only used for the initial master contract.\n /// @notice Subsequent clones are initialised via `init`.\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n masterContract = this;\n }\n\n /// @notice De facto constructor for clone contracts\n function init(bytes calldata data) public payable override {\n require(address(collateral) == address(0), \"NFTPair: already initialized\");\n (collateral, asset) = abi.decode(data, (IERC721, IERC20));\n require(address(collateral) != address(0), \"NFTPair: bad pair\");\n }\n\n function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public {\n TokenLoan memory loan = tokenLoan[tokenId];\n if (loan.status == LOAN_OUTSTANDING) {\n // The lender can change terms so long as the changes are strictly\n // the same or better for the borrower:\n require(msg.sender == loan.lender, \"NFTPair: not the lender\");\n TokenLoanParams memory cur = tokenLoanParams[tokenId];\n require(\n params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS,\n \"NFTPair: worse params\"\n );\n } else if (loan.status == LOAN_REQUESTED) {\n // The borrower has already deposited the collateral and can\n // change whatever they like\n require(msg.sender == loan.borrower, \"NFTPair: not the borrower\");\n } else {\n // The loan has not been taken out yet; the borrower needs to\n // provide collateral.\n revert(\"NFTPair: no collateral\");\n }\n tokenLoanParams[tokenId] = params;\n emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS);\n }\n\n function _requestLoan(\n address collateralProvider,\n uint256 tokenId,\n TokenLoanParams memory params,\n address to,\n bool skim\n ) private {\n // Edge case: valuation can be zero. That effectively gifts the NFT and\n // is therefore a bad idea, but does not break the contract.\n require(tokenLoan[tokenId].status == LOAN_INITIAL, \"NFTPair: loan exists\");\n if (skim) {\n require(collateral.ownerOf(tokenId) == address(this), \"NFTPair: skim failed\");\n } else {\n collateral.transferFrom(collateralProvider, address(this), tokenId);\n }\n TokenLoan memory loan;\n loan.borrower = to;\n loan.status = LOAN_REQUESTED;\n tokenLoan[tokenId] = loan;\n tokenLoanParams[tokenId] = params;\n\n emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS);\n }\n\n /// @notice Deposit an NFT as collateral and request a loan against it\n /// @param tokenId ID of the NFT\n /// @param to Address to receive the loan, or option to withdraw collateral\n /// @param params Loan conditions on offer\n /// @param skim True if the token has already been transfered\n function requestLoan(\n uint256 tokenId,\n TokenLoanParams memory params,\n address to,\n bool skim\n ) public {\n _requestLoan(msg.sender, tokenId, params, to, skim);\n }\n\n /// @notice Removes `tokenId` as collateral and transfers it to `to`.\n /// @notice This destroys the loan.\n /// @param tokenId The token\n /// @param to The receiver of the token.\n function removeCollateral(uint256 tokenId, address to) public {\n TokenLoan memory loan = tokenLoan[tokenId];\n if (loan.status == LOAN_REQUESTED) {\n // We are withdrawing collateral that is not in use:\n require(msg.sender == loan.borrower, \"NFTPair: not the borrower\");\n } else if (loan.status == LOAN_OUTSTANDING) {\n // We are seizing collateral as the lender. The loan has to be\n // expired and not paid off:\n require(msg.sender == loan.lender, \"NFTPair: not the lender\");\n require(\n // Addition is safe: both summands are smaller than 256 bits\n uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp,\n \"NFTPair: not expired\"\n );\n }\n // If there somehow is collateral but no accompanying loan, then anyone\n // can claim it by first requesting a loan with `skim` set to true, and\n // then withdrawing. So we might as well allow it here..\n delete tokenLoan[tokenId];\n collateral.transferFrom(address(this), to, tokenId);\n emit LogRemoveCollateral(tokenId, to);\n }\n\n // Assumes the lender has agreed to the loan.\n function _lend(\n address lender,\n uint256 tokenId,\n TokenLoanParams memory accepted,\n bool skim\n ) internal {\n TokenLoan memory loan = tokenLoan[tokenId];\n require(loan.status == LOAN_REQUESTED, \"NFTPair: not available\");\n TokenLoanParams memory params = tokenLoanParams[tokenId];\n\n // Valuation has to be an exact match, everything else must be at least\n // as good for the lender as `accepted`.\n require(\n params.valuation == accepted.valuation &&\n params.duration <= accepted.duration &&\n params.annualInterestBPS >= accepted.annualInterestBPS,\n \"NFTPair: bad params\"\n );\n\n uint256 totalShare = bentoBox.toShare(asset, params.valuation, false);\n // No overflow: at most 128 + 16 bits (fits in BentoBox)\n uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS;\n uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS;\n\n if (skim) {\n require(\n bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare),\n \"NFTPair: skim too much\"\n );\n } else {\n bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare);\n }\n // No underflow: follows from OPEN_FEE_BPS <= BPS\n uint256 borrowerShare = totalShare - openFeeShare;\n bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare);\n // No overflow: addends (and result) must fit in BentoBox\n feesEarnedShare += protocolFeeShare;\n\n loan.lender = lender;\n loan.status = LOAN_OUTSTANDING;\n loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years..\n tokenLoan[tokenId] = loan;\n\n emit LogLend(lender, tokenId);\n }\n\n /// @notice Lends with the parameters specified by the borrower.\n /// @param tokenId ID of the token that will function as collateral\n /// @param accepted Loan parameters as the lender saw them, for security\n /// @param skim True if the funds have been transfered to the contract\n function lend(\n uint256 tokenId,\n TokenLoanParams memory accepted,\n bool skim\n ) public {\n _lend(msg.sender, tokenId, accepted, skim);\n }\n\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return _domainSeparator();\n }\n\n // NOTE on signature hashes: the domain separator only guarantees that the\n // chain ID and master contract are a match, so we explicitly include the\n // clone address (and the asset/collateral addresses):\n\n // keccak256(\"Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\")\n bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8;\n\n // keccak256(\"Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)\")\n bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336;\n\n /// @notice Request and immediately borrow from a pre-committed lender\n\n /// @notice Caller provides collateral; loan can go to a different address.\n /// @param tokenId ID of the token that will function as collateral\n /// @param lender Lender, whose BentoBox balance the funds will come from\n /// @param recipient Address to receive the loan.\n /// @param params Loan parameters requested, and signed by the lender\n /// @param skimCollateral True if the collateral has already been transfered\n /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature.\n function requestAndBorrow(\n uint256 tokenId,\n address lender,\n address recipient,\n TokenLoanParams memory params,\n bool skimCollateral,\n bool anyTokenId,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n if (v == 0 && r == bytes32(0) && s == bytes32(0)) {\n require(ILendingClub(lender).willLend(tokenId, params), \"NFTPair: LendingClub does not like you\");\n } else {\n require(block.timestamp <= deadline, \"NFTPair: signature expired\");\n uint256 nonce = nonces[lender]++;\n bytes32 dataHash = keccak256(\n abi.encode(\n LEND_SIGNATURE_HASH,\n address(this),\n anyTokenId ? 0 : tokenId,\n anyTokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS,\n nonce,\n deadline\n )\n );\n require(ecrecover(_getDigest(dataHash), v, r, s) == lender, \"NFTPair: signature invalid\");\n }\n _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral);\n _lend(lender, tokenId, params, false);\n }\n\n /// @notice Take collateral from a pre-commited borrower and lend against it\n /// @notice Collateral must come from the borrower, not a third party.\n /// @param tokenId ID of the token that will function as collateral\n /// @param borrower Address that provides collateral and receives the loan\n /// @param params Loan terms offered, and signed by the borrower\n /// @param skimFunds True if the funds have been transfered to the contract\n function takeCollateralAndLend(\n uint256 tokenId,\n address borrower,\n TokenLoanParams memory params,\n bool skimFunds,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) public {\n require(block.timestamp <= deadline, \"NFTPair: signature expired\");\n uint256 nonce = nonces[borrower]++;\n bytes32 dataHash = keccak256(\n abi.encode(\n BORROW_SIGNATURE_HASH,\n address(this),\n tokenId,\n params.valuation,\n params.duration,\n params.annualInterestBPS,\n nonce,\n deadline\n )\n );\n require(ecrecover(_getDigest(dataHash), v, r, s) == borrower, \"NFTPair: signature invalid\");\n _requestLoan(borrower, tokenId, params, borrower, false);\n _lend(msg.sender, tokenId, params, skimFunds);\n }\n\n /// Approximates continuous compounding. Uses Horner's method to evaluate\n /// the truncated Maclaurin series for exp - 1, accumulating rounding\n /// errors along the way. The following is always guaranteed:\n ///\n /// principal * time * apr <= result <= principal * (e^(time * apr) - 1),\n ///\n /// where time = t/YEAR, up to at most the rounding error obtained in\n /// calculating linear interest.\n ///\n /// If the theoretical result that we are approximating (the rightmost part\n /// of the above inquality) fits in 128 bits, then the function is\n /// guaranteed not to revert (unless n > 250, which is way too high).\n /// If even the linear interest (leftmost part of the inequality) does not\n /// the function will revert.\n /// Otherwise, the function may revert, return a reasonable result, or\n /// return a very inaccurate result. Even then the above inequality is\n /// respected.\n function calculateInterest(\n uint256 principal,\n uint64 t,\n uint16 aprBPS\n ) public pure returns (uint256 interest) {\n // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS)\n //\n // We calculate\n //\n // ----- n ----- n\n // \\ principal * (t * aprBPS)^k \\\n // ) -------------------------- =: ) term_k\n // / k! * YEAR_BPS^k /\n // ----- k = 1 ----- k = 1\n //\n // which approaches, but never exceeds the \"theoretical\" result,\n //\n // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1\n //\n // as n goes to infinity. We use the fact that\n //\n // principal * (t * aprBPS)^(k-1) * (t * aprBPS)\n // term_k = ---------------------------------------------\n // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS\n //\n // t * aprBPS\n // = term_{k-1} * ------------ (*)\n // k * YEAR_BPS\n //\n // to calculate the terms one by one. The principal affords us the\n // precision to carry out the division without resorting to fixed-point\n // math. Any rounding error is downward, which we consider acceptable.\n //\n // Since all numbers involved are positive, each term is certainly\n // bounded above by M. From (*) we see that any intermediate results\n // are at most\n //\n // denom_k := k * YEAR_BPS.\n //\n // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits,\n // which proves that all calculations will certainly not overflow if M\n // fits in 128 bits.\n //\n // If M does not fit, then the intermediate results for some term may\n // eventually overflow, but this cannot happen at the first term, and\n // neither can the total overflow because it uses checked math.\n //\n // This constitutes a guarantee of specified behavior when M >= 2^128.\n uint256 x = uint256(t) * aprBPS;\n uint256 term_k = (principal * x) / YEAR_BPS;\n uint256 denom_k = YEAR_BPS;\n\n interest = term_k;\n for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) {\n denom_k += YEAR_BPS;\n term_k = (term_k * x) / denom_k;\n interest = interest.add(term_k); // <- Only overflow check we need\n }\n\n if (interest >= 2**128) {\n revert();\n }\n }\n\n function repay(uint256 tokenId, bool skim) public returns (uint256 amount) {\n TokenLoan memory loan = tokenLoan[tokenId];\n require(loan.status == LOAN_OUTSTANDING, \"NFTPair: no loan\");\n TokenLoanParams memory loanParams = tokenLoanParams[tokenId];\n require(\n // Addition is safe: both summands are smaller than 256 bits\n uint256(loan.startTime) + loanParams.duration > block.timestamp,\n \"NFTPair: loan expired\"\n );\n\n uint128 principal = loanParams.valuation;\n\n // No underflow: loan.startTime is only ever set to a block timestamp\n // Cast is safe: if this overflows, then all loans have expired anyway\n uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128();\n uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS;\n amount = principal + interest;\n\n uint256 totalShare = bentoBox.toShare(asset, amount, false);\n uint256 feeShare = bentoBox.toShare(asset, fee, false);\n\n address from;\n if (skim) {\n require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), \"NFTPair: skim too much\");\n from = address(this);\n // No overflow: result fits in BentoBox\n } else {\n bentoBox.transfer(asset, msg.sender, address(this), feeShare);\n from = msg.sender;\n }\n // No underflow: PROTOCOL_FEE_BPS < BPS by construction.\n feesEarnedShare += feeShare;\n delete tokenLoan[tokenId];\n\n bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare);\n collateral.transferFrom(address(this), loan.borrower, tokenId);\n\n emit LogRepay(from, tokenId);\n }\n\n uint8 internal constant ACTION_REPAY = 2;\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\n\n uint8 internal constant ACTION_REQUEST_LOAN = 12;\n uint8 internal constant ACTION_LEND = 13;\n\n // Function on BentoBox\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\n\n // Any external call (except to BentoBox)\n uint8 internal constant ACTION_CALL = 30;\n\n // Signed requests\n uint8 internal constant ACTION_REQUEST_AND_BORROW = 40;\n uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41;\n\n int256 internal constant USE_VALUE1 = -1;\n int256 internal constant USE_VALUE2 = -2;\n\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\n function _num(\n int256 inNum,\n uint256 value1,\n uint256 value2\n ) internal pure returns (uint256 outNum) {\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\n }\n\n /// @dev Helper function for depositing into `bentoBox`.\n function _bentoDeposit(\n bytes memory data,\n uint256 value,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\n share = int256(_num(share, value1, value2));\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\n }\n\n /// @dev Helper function to withdraw from the `bentoBox`.\n function _bentoWithdraw(\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (uint256, uint256) {\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\n }\n\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\n /// Calls to `bentoBox` or `collateral` are not allowed for security reasons.\n /// This also means that calls made from this contract shall *not* be trusted.\n function _call(\n uint256 value,\n bytes memory data,\n uint256 value1,\n uint256 value2\n ) internal returns (bytes memory, uint8) {\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode(\n data,\n (address, bytes, bool, bool, uint8)\n );\n\n if (useValue1 && !useValue2) {\n callData = abi.encodePacked(callData, value1);\n } else if (!useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value2);\n } else if (useValue1 && useValue2) {\n callData = abi.encodePacked(callData, value1, value2);\n }\n\n require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), \"NFTPair: can't call\");\n\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\n require(success, \"NFTPair: call failed\");\n return (returnData, returnValues);\n }\n\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\n function cook(\n uint8[] calldata actions,\n uint256[] calldata values,\n bytes[] calldata datas\n ) external payable returns (uint256 value1, uint256 value2) {\n for (uint256 i = 0; i < actions.length; i++) {\n uint8 action = actions[i];\n if (action == ACTION_REPAY) {\n (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool));\n repay(tokenId, skim);\n } else if (action == ACTION_REMOVE_COLLATERAL) {\n (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address));\n removeCollateral(tokenId, to);\n } else if (action == ACTION_REQUEST_LOAN) {\n (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode(\n datas[i],\n (uint256, TokenLoanParams, address, bool)\n );\n requestLoan(tokenId, params, to, skim);\n } else if (action == ACTION_LEND) {\n (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool));\n lend(tokenId, params, skim);\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode(\n datas[i],\n (address, address, bool, uint8, bytes32, bytes32)\n );\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\n } else if (action == ACTION_BENTO_DEPOSIT) {\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\n } else if (action == ACTION_BENTO_WITHDRAW) {\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\n } else if (action == ACTION_BENTO_TRANSFER) {\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\n } else if (action == ACTION_CALL) {\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\n\n if (returnValues == 1) {\n (value1) = abi.decode(returnData, (uint256));\n } else if (returnValues == 2) {\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\n }\n } else if (action == ACTION_REQUEST_AND_BORROW) {\n (\n uint256 tokenId,\n address lender,\n address recipient,\n TokenLoanParams memory params,\n bool skimCollateral,\n bool anyTokenId,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, uint256, uint8, bytes32, bytes32));\n requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, deadline, v, r, s);\n } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) {\n (\n uint256 tokenId,\n address borrower,\n TokenLoanParams memory params,\n bool skimFunds,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32));\n takeCollateralAndLend(tokenId, borrower, params, skimFunds, deadline, v, r, s);\n }\n }\n }\n\n /// @notice Withdraws the fees accumulated.\n function withdrawFees() public {\n address to = masterContract.feeTo();\n\n uint256 _share = feesEarnedShare;\n if (_share > 0) {\n bentoBox.transfer(asset, address(this), to, _share);\n feesEarnedShare = 0;\n }\n\n emit LogWithdrawFees(to, _share);\n }\n\n /// @notice Sets the beneficiary of fees accrued in liquidations.\n /// MasterContract Only Admin function.\n /// @param newFeeTo The address of the receiver.\n function setFeeTo(address newFeeTo) public onlyOwner {\n feeTo = newFeeTo;\n emit LogFeeTo(newFeeTo);\n }\n}\n" + }, + "contracts/interfaces/IERC721.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Taken from OpenZeppelin contracts v3\n\npragma solidity >=0.6.2 <0.8.0;\n\nimport \"./IERC165.sol\";\n\n/**\n * @dev Required interface of an ERC721 compliant contract.\n */\ninterface IERC721 is IERC165 {\n /**\n * @dev Emitted when `tokenId` token is transferred from `from` to `to`.\n */\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.\n */\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n\n /**\n * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.\n */\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n\n /**\n * @dev Returns the number of tokens in ``owner``'s account.\n */\n function balanceOf(address owner) external view returns (uint256 balance);\n\n /**\n * @dev Returns the owner of the `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function ownerOf(uint256 tokenId) external view returns (address owner);\n\n /**\n * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients\n * are aware of the ERC721 protocol to prevent tokens from being forever locked.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must exist and be owned by `from`.\n * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\n *\n * Emits a {Transfer} event.\n */\n function safeTransferFrom(address from, address to, uint256 tokenId) external;\n\n /**\n * @dev Transfers `tokenId` token from `from` to `to`.\n *\n * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must be owned by `from`.\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 tokenId) external;\n\n /**\n * @dev Gives permission to `to` to transfer `tokenId` token to another account.\n * The approval is cleared when the token is transferred.\n *\n * Only a single account can be approved at a time, so approving the zero address clears previous approvals.\n *\n * Requirements:\n *\n * - The caller must own the token or be an approved operator.\n * - `tokenId` must exist.\n *\n * Emits an {Approval} event.\n */\n function approve(address to, uint256 tokenId) external;\n\n /**\n * @dev Returns the account approved for `tokenId` token.\n *\n * Requirements:\n *\n * - `tokenId` must exist.\n */\n function getApproved(uint256 tokenId) external view returns (address operator);\n\n /**\n * @dev Approve or remove `operator` as an operator for the caller.\n * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.\n *\n * Requirements:\n *\n * - The `operator` cannot be the caller.\n *\n * Emits an {ApprovalForAll} event.\n */\n function setApprovalForAll(address operator, bool _approved) external;\n\n /**\n * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.\n *\n * See {setApprovalForAll}\n */\n function isApprovedForAll(address owner, address operator) external view returns (bool);\n\n /**\n * @dev Safely transfers `tokenId` token from `from` to `to`.\n *\n * Requirements:\n *\n * - `from` cannot be the zero address.\n * - `to` cannot be the zero address.\n * - `tokenId` token must exist and be owned by `from`.\n * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.\n * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.\n *\n * Emits a {Transfer} event.\n */\n function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\n}\n" + }, + "contracts/interfaces/IERC165.sol": { + "content": "// SPDX-License-Identifier: MIT\n// Taken from OpenZeppelin contracts v3\n\npragma solidity >=0.6.0 <0.8.0;\n\n/**\n * @dev Interface of the ERC165 standard, as defined in the\n * https://eips.ethereum.org/EIPS/eip-165[EIP].\n *\n * Implementers can declare support of contract interfaces, which can then be\n * queried by others ({ERC165Checker}).\n *\n * For an implementation, see {ERC165}.\n */\ninterface IERC165 {\n /**\n * @dev Returns true if this contract implements the interface defined by\n * `interfaceId`. See the corresponding\n * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]\n * to learn more about how these ids are created.\n *\n * This function call must use less than 30 000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n" + }, + "contracts/CauldronV2.sol": { + "content": "// SPDX-License-Identifier: UNLICENSED\r\n\r\n// Cauldron\r\n\r\n// ( ( (\r\n// )\\ ) ( )\\ )\\ ) (\r\n// (((_) ( /( ))\\ ((_)(()/( )( ( (\r\n// )\\___ )(_)) /((_) _ ((_))(()\\ )\\ )\\ )\r\n// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/(\r\n// | (__ / _` || || || |/ _` | | '_|/ _ \\| ' \\))\r\n// \\___|\\__,_| \\_,_||_|\\__,_| |_| \\___/|_||_|\r\n\r\n// Copyright (c) 2021 BoringCrypto - All rights reserved\r\n// Twitter: @Boring_Crypto\r\n\r\n// Special thanks to:\r\n// @0xKeno - for all his invaluable contributions\r\n// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations\r\n\r\npragma solidity 0.6.12;\r\npragma experimental ABIEncoderV2;\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol\";\r\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\r\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\r\nimport \"./MagicInternetMoney.sol\";\r\nimport \"./interfaces/IOracle.sol\";\r\nimport \"./interfaces/ISwapper.sol\";\r\n\r\n// solhint-disable avoid-low-level-calls\r\n// solhint-disable no-inline-assembly\r\n\r\n/// @title Cauldron\r\n/// @dev This contract allows contract calls to any contract (except BentoBox)\r\n/// from arbitrary callers thus, don't trust calls from this contract in any circumstances.\r\ncontract CauldronV2 is BoringOwnable, IMasterContract {\r\n using BoringMath for uint256;\r\n using BoringMath128 for uint128;\r\n using RebaseLibrary for Rebase;\r\n using BoringERC20 for IERC20;\r\n\r\n event LogExchangeRate(uint256 rate);\r\n event LogAccrue(uint128 accruedAmount);\r\n event LogAddCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogRemoveCollateral(address indexed from, address indexed to, uint256 share);\r\n event LogBorrow(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogRepay(address indexed from, address indexed to, uint256 amount, uint256 part);\r\n event LogFeeTo(address indexed newFeeTo);\r\n event LogWithdrawFees(address indexed feeTo, uint256 feesEarnedFraction);\r\n\r\n // Immutables (for MasterContract and all clones)\r\n IBentoBoxV1 public immutable bentoBox;\r\n CauldronV2 public immutable masterContract;\r\n IERC20 public immutable magicInternetMoney;\r\n\r\n // MasterContract variables\r\n address public feeTo;\r\n\r\n // Per clone variables\r\n // Clone init settings\r\n IERC20 public collateral;\r\n IOracle public oracle;\r\n bytes public oracleData;\r\n\r\n // Total amounts\r\n uint256 public totalCollateralShare; // Total collateral supplied\r\n Rebase public totalBorrow; // elastic = Total token amount to be repayed by borrowers, base = Total parts of the debt held by borrowers\r\n\r\n // User balances\r\n mapping(address => uint256) public userCollateralShare;\r\n mapping(address => uint256) public userBorrowPart;\r\n\r\n /// @notice Exchange and interest rate tracking.\r\n /// This is 'cached' here because calls to Oracles can be very expensive.\r\n uint256 public exchangeRate;\r\n\r\n struct AccrueInfo {\r\n uint64 lastAccrued;\r\n uint128 feesEarned;\r\n uint64 INTEREST_PER_SECOND;\r\n }\r\n\r\n AccrueInfo public accrueInfo;\r\n\r\n // Settings\r\n uint256 public COLLATERIZATION_RATE;\r\n uint256 private constant COLLATERIZATION_RATE_PRECISION = 1e5; // Must be less than EXCHANGE_RATE_PRECISION (due to optimization in math)\r\n\r\n uint256 private constant EXCHANGE_RATE_PRECISION = 1e18;\r\n\r\n uint256 public LIQUIDATION_MULTIPLIER; \r\n uint256 private constant LIQUIDATION_MULTIPLIER_PRECISION = 1e5;\r\n\r\n uint256 public BORROW_OPENING_FEE;\r\n uint256 private constant BORROW_OPENING_FEE_PRECISION = 1e5;\r\n\r\n uint256 private constant DISTRIBUTION_PART = 10;\r\n uint256 private constant DISTRIBUTION_PRECISION = 100;\r\n\r\n /// @notice The constructor is only used for the initial master contract. Subsequent clones are initialised via `init`.\r\n constructor(IBentoBoxV1 bentoBox_, IERC20 magicInternetMoney_) public {\r\n bentoBox = bentoBox_;\r\n magicInternetMoney = magicInternetMoney_;\r\n masterContract = this;\r\n }\r\n\r\n /// @notice Serves as the constructor for clones, as clones can't have a regular constructor\r\n /// @dev `data` is abi encoded in the format: (IERC20 collateral, IERC20 asset, IOracle oracle, bytes oracleData)\r\n function init(bytes calldata data) public payable override {\r\n require(address(collateral) == address(0), \"Cauldron: already initialized\");\r\n (collateral, oracle, oracleData, accrueInfo.INTEREST_PER_SECOND, LIQUIDATION_MULTIPLIER, COLLATERIZATION_RATE, BORROW_OPENING_FEE) = abi.decode(data, (IERC20, IOracle, bytes, uint64, uint256, uint256, uint256));\r\n require(address(collateral) != address(0), \"Cauldron: bad pair\");\r\n }\r\n\r\n /// @notice Accrues the interest on the borrowed tokens and handles the accumulation of fees.\r\n function accrue() public {\r\n AccrueInfo memory _accrueInfo = accrueInfo;\r\n // Number of seconds since accrue was called\r\n uint256 elapsedTime = block.timestamp - _accrueInfo.lastAccrued;\r\n if (elapsedTime == 0) {\r\n return;\r\n }\r\n _accrueInfo.lastAccrued = uint64(block.timestamp);\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n if (_totalBorrow.base == 0) {\r\n accrueInfo = _accrueInfo;\r\n return;\r\n }\r\n\r\n // Accrue interest\r\n uint128 extraAmount = (uint256(_totalBorrow.elastic).mul(_accrueInfo.INTEREST_PER_SECOND).mul(elapsedTime) / 1e18).to128();\r\n _totalBorrow.elastic = _totalBorrow.elastic.add(extraAmount);\r\n\r\n _accrueInfo.feesEarned = _accrueInfo.feesEarned.add(extraAmount);\r\n totalBorrow = _totalBorrow;\r\n accrueInfo = _accrueInfo;\r\n\r\n emit LogAccrue(extraAmount);\r\n }\r\n\r\n /// @notice Concrete implementation of `isSolvent`. Includes a third parameter to allow caching `exchangeRate`.\r\n /// @param _exchangeRate The exchange rate. Used to cache the `exchangeRate` between calls.\r\n function _isSolvent(address user, uint256 _exchangeRate) internal view returns (bool) {\r\n // accrue must have already been called!\r\n uint256 borrowPart = userBorrowPart[user];\r\n if (borrowPart == 0) return true;\r\n uint256 collateralShare = userCollateralShare[user];\r\n if (collateralShare == 0) return false;\r\n\r\n Rebase memory _totalBorrow = totalBorrow;\r\n\r\n return\r\n bentoBox.toAmount(\r\n collateral,\r\n collateralShare.mul(EXCHANGE_RATE_PRECISION / COLLATERIZATION_RATE_PRECISION).mul(COLLATERIZATION_RATE),\r\n false\r\n ) >=\r\n // Moved exchangeRate here instead of dividing the other side to preserve more precision\r\n borrowPart.mul(_totalBorrow.elastic).mul(_exchangeRate) / _totalBorrow.base;\r\n }\r\n\r\n /// @dev Checks if the user is solvent in the closed liquidation case at the end of the function body.\r\n modifier solvent() {\r\n _;\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n\r\n /// @notice Gets the exchange rate. I.e how much collateral to buy 1e18 asset.\r\n /// This function is supposed to be invoked if needed because Oracle queries can be expensive.\r\n /// @return updated True if `exchangeRate` was updated.\r\n /// @return rate The new exchange rate.\r\n function updateExchangeRate() public returns (bool updated, uint256 rate) {\r\n (updated, rate) = oracle.get(oracleData);\r\n\r\n if (updated) {\r\n exchangeRate = rate;\r\n emit LogExchangeRate(rate);\r\n } else {\r\n // Return the old rate if fetching wasn't successful\r\n rate = exchangeRate;\r\n }\r\n }\r\n\r\n /// @dev Helper function to move tokens.\r\n /// @param token The ERC-20 token.\r\n /// @param share The amount in shares to add.\r\n /// @param total Grand total amount to deduct from this contract's balance. Only applicable if `skim` is True.\r\n /// Only used for accounting checks.\r\n /// @param skim If True, only does a balance check on this contract.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n function _addTokens(\r\n IERC20 token,\r\n uint256 share,\r\n uint256 total,\r\n bool skim\r\n ) internal {\r\n if (skim) {\r\n require(share <= bentoBox.balanceOf(token, address(this)).sub(total), \"Cauldron: Skim too much\");\r\n } else {\r\n bentoBox.transfer(token, msg.sender, address(this), share);\r\n }\r\n }\r\n\r\n /// @notice Adds `collateral` from msg.sender to the account `to`.\r\n /// @param to The receiver of the tokens.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.x\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param share The amount of shares to add for `to`.\r\n function addCollateral(\r\n address to,\r\n bool skim,\r\n uint256 share\r\n ) public {\r\n userCollateralShare[to] = userCollateralShare[to].add(share);\r\n uint256 oldTotalCollateralShare = totalCollateralShare;\r\n totalCollateralShare = oldTotalCollateralShare.add(share);\r\n _addTokens(collateral, share, oldTotalCollateralShare, skim);\r\n emit LogAddCollateral(skim ? address(bentoBox) : msg.sender, to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `removeCollateral`.\r\n function _removeCollateral(address to, uint256 share) internal {\r\n userCollateralShare[msg.sender] = userCollateralShare[msg.sender].sub(share);\r\n totalCollateralShare = totalCollateralShare.sub(share);\r\n emit LogRemoveCollateral(msg.sender, to, share);\r\n bentoBox.transfer(collateral, address(this), to, share);\r\n }\r\n\r\n /// @notice Removes `share` amount of collateral and transfers it to `to`.\r\n /// @param to The receiver of the shares.\r\n /// @param share Amount of shares to remove.\r\n function removeCollateral(address to, uint256 share) public solvent {\r\n // accrue must be called because we check solvency\r\n accrue();\r\n _removeCollateral(to, share);\r\n }\r\n\r\n /// @dev Concrete implementation of `borrow`.\r\n function _borrow(address to, uint256 amount) internal returns (uint256 part, uint256 share) {\r\n uint256 feeAmount = amount.mul(BORROW_OPENING_FEE) / BORROW_OPENING_FEE_PRECISION; // A flat % fee is charged for any borrow\r\n (totalBorrow, part) = totalBorrow.add(amount.add(feeAmount), true);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(uint128(feeAmount));\r\n userBorrowPart[msg.sender] = userBorrowPart[msg.sender].add(part);\r\n\r\n // As long as there are tokens on this contract you can 'mint'... this enables limiting borrows\r\n share = bentoBox.toShare(magicInternetMoney, amount, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), to, share);\r\n\r\n emit LogBorrow(msg.sender, to, amount.add(feeAmount), part);\r\n }\r\n\r\n /// @notice Sender borrows `amount` and transfers it to `to`.\r\n /// @return part Total part of the debt held by borrowers.\r\n /// @return share Total amount in shares borrowed.\r\n function borrow(address to, uint256 amount) public solvent returns (uint256 part, uint256 share) {\r\n accrue();\r\n (part, share) = _borrow(to, amount);\r\n }\r\n\r\n /// @dev Concrete implementation of `repay`.\r\n function _repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) internal returns (uint256 amount) {\r\n (totalBorrow, amount) = totalBorrow.sub(part, true);\r\n userBorrowPart[to] = userBorrowPart[to].sub(part);\r\n\r\n uint256 share = bentoBox.toShare(magicInternetMoney, amount, true);\r\n bentoBox.transfer(magicInternetMoney, skim ? address(bentoBox) : msg.sender, address(this), share);\r\n emit LogRepay(skim ? address(bentoBox) : msg.sender, to, amount, part);\r\n }\r\n\r\n /// @notice Repays a loan.\r\n /// @param to Address of the user this payment should go.\r\n /// @param skim True if the amount should be skimmed from the deposit balance of msg.sender.\r\n /// False if tokens from msg.sender in `bentoBox` should be transferred.\r\n /// @param part The amount to repay. See `userBorrowPart`.\r\n /// @return amount The total amount repayed.\r\n function repay(\r\n address to,\r\n bool skim,\r\n uint256 part\r\n ) public returns (uint256 amount) {\r\n accrue();\r\n amount = _repay(to, skim, part);\r\n }\r\n\r\n // Functions that need accrue to be called\r\n uint8 internal constant ACTION_REPAY = 2;\r\n uint8 internal constant ACTION_REMOVE_COLLATERAL = 4;\r\n uint8 internal constant ACTION_BORROW = 5;\r\n uint8 internal constant ACTION_GET_REPAY_SHARE = 6;\r\n uint8 internal constant ACTION_GET_REPAY_PART = 7;\r\n uint8 internal constant ACTION_ACCRUE = 8;\r\n\r\n // Functions that don't need accrue to be called\r\n uint8 internal constant ACTION_ADD_COLLATERAL = 10;\r\n uint8 internal constant ACTION_UPDATE_EXCHANGE_RATE = 11;\r\n\r\n // Function on BentoBox\r\n uint8 internal constant ACTION_BENTO_DEPOSIT = 20;\r\n uint8 internal constant ACTION_BENTO_WITHDRAW = 21;\r\n uint8 internal constant ACTION_BENTO_TRANSFER = 22;\r\n uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23;\r\n uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24;\r\n\r\n // Any external call (except to BentoBox)\r\n uint8 internal constant ACTION_CALL = 30;\r\n\r\n int256 internal constant USE_VALUE1 = -1;\r\n int256 internal constant USE_VALUE2 = -2;\r\n\r\n /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`.\r\n function _num(\r\n int256 inNum,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal pure returns (uint256 outNum) {\r\n outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2);\r\n }\r\n\r\n /// @dev Helper function for depositing into `bentoBox`.\r\n function _bentoDeposit(\r\n bytes memory data,\r\n uint256 value,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors\r\n share = int256(_num(share, value1, value2));\r\n return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share));\r\n }\r\n\r\n /// @dev Helper function to withdraw from the `bentoBox`.\r\n function _bentoWithdraw(\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (uint256, uint256) {\r\n (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256));\r\n return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2));\r\n }\r\n\r\n /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure.\r\n /// Calls to `bentoBox` are not allowed for obvious security reasons.\r\n /// This also means that calls made from this contract shall *not* be trusted.\r\n function _call(\r\n uint256 value,\r\n bytes memory data,\r\n uint256 value1,\r\n uint256 value2\r\n ) internal returns (bytes memory, uint8) {\r\n (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =\r\n abi.decode(data, (address, bytes, bool, bool, uint8));\r\n\r\n if (useValue1 && !useValue2) {\r\n callData = abi.encodePacked(callData, value1);\r\n } else if (!useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value2);\r\n } else if (useValue1 && useValue2) {\r\n callData = abi.encodePacked(callData, value1, value2);\r\n }\r\n\r\n require(callee != address(bentoBox) && callee != address(this), \"Cauldron: can't call\");\r\n\r\n (bool success, bytes memory returnData) = callee.call{value: value}(callData);\r\n require(success, \"Cauldron: call failed\");\r\n return (returnData, returnValues);\r\n }\r\n\r\n struct CookStatus {\r\n bool needsSolvencyCheck;\r\n bool hasAccrued;\r\n }\r\n\r\n /// @notice Executes a set of actions and allows composability (contract calls) to other contracts.\r\n /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations).\r\n /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions.\r\n /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`.\r\n /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments.\r\n /// @return value1 May contain the first positioned return value of the last executed action (if applicable).\r\n /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable).\r\n function cook(\r\n uint8[] calldata actions,\r\n uint256[] calldata values,\r\n bytes[] calldata datas\r\n ) external payable returns (uint256 value1, uint256 value2) {\r\n CookStatus memory status;\r\n for (uint256 i = 0; i < actions.length; i++) {\r\n uint8 action = actions[i];\r\n if (!status.hasAccrued && action < 10) {\r\n accrue();\r\n status.hasAccrued = true;\r\n }\r\n if (action == ACTION_ADD_COLLATERAL) {\r\n (int256 share, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n addCollateral(to, skim, _num(share, value1, value2));\r\n } else if (action == ACTION_REPAY) {\r\n (int256 part, address to, bool skim) = abi.decode(datas[i], (int256, address, bool));\r\n _repay(to, skim, _num(part, value1, value2));\r\n } else if (action == ACTION_REMOVE_COLLATERAL) {\r\n (int256 share, address to) = abi.decode(datas[i], (int256, address));\r\n _removeCollateral(to, _num(share, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_BORROW) {\r\n (int256 amount, address to) = abi.decode(datas[i], (int256, address));\r\n (value1, value2) = _borrow(to, _num(amount, value1, value2));\r\n status.needsSolvencyCheck = true;\r\n } else if (action == ACTION_UPDATE_EXCHANGE_RATE) {\r\n (bool must_update, uint256 minRate, uint256 maxRate) = abi.decode(datas[i], (bool, uint256, uint256));\r\n (bool updated, uint256 rate) = updateExchangeRate();\r\n require((!must_update || updated) && rate > minRate && (maxRate == 0 || rate > maxRate), \"Cauldron: rate not ok\");\r\n } else if (action == ACTION_BENTO_SETAPPROVAL) {\r\n (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) =\r\n abi.decode(datas[i], (address, address, bool, uint8, bytes32, bytes32));\r\n bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s);\r\n } else if (action == ACTION_BENTO_DEPOSIT) {\r\n (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2);\r\n } else if (action == ACTION_BENTO_WITHDRAW) {\r\n (value1, value2) = _bentoWithdraw(datas[i], value1, value2);\r\n } else if (action == ACTION_BENTO_TRANSFER) {\r\n (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256));\r\n bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2));\r\n } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) {\r\n (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[]));\r\n bentoBox.transferMultiple(token, msg.sender, tos, shares);\r\n } else if (action == ACTION_CALL) {\r\n (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);\r\n\r\n if (returnValues == 1) {\r\n (value1) = abi.decode(returnData, (uint256));\r\n } else if (returnValues == 2) {\r\n (value1, value2) = abi.decode(returnData, (uint256, uint256));\r\n }\r\n } else if (action == ACTION_GET_REPAY_SHARE) {\r\n int256 part = abi.decode(datas[i], (int256));\r\n value1 = bentoBox.toShare(magicInternetMoney, totalBorrow.toElastic(_num(part, value1, value2), true), true);\r\n } else if (action == ACTION_GET_REPAY_PART) {\r\n int256 amount = abi.decode(datas[i], (int256));\r\n value1 = totalBorrow.toBase(_num(amount, value1, value2), false);\r\n }\r\n }\r\n\r\n if (status.needsSolvencyCheck) {\r\n require(_isSolvent(msg.sender, exchangeRate), \"Cauldron: user insolvent\");\r\n }\r\n }\r\n\r\n /// @notice Handles the liquidation of users' balances, once the users' amount of collateral is too low.\r\n /// @param users An array of user addresses.\r\n /// @param maxBorrowParts A one-to-one mapping to `users`, contains maximum (partial) borrow amounts (to liquidate) of the respective user.\r\n /// @param to Address of the receiver in open liquidations if `swapper` is zero.\r\n function liquidate(\r\n address[] calldata users,\r\n uint256[] calldata maxBorrowParts,\r\n address to,\r\n ISwapper swapper\r\n ) public {\r\n // Oracle can fail but we still need to allow liquidations\r\n (, uint256 _exchangeRate) = updateExchangeRate();\r\n accrue();\r\n\r\n uint256 allCollateralShare;\r\n uint256 allBorrowAmount;\r\n uint256 allBorrowPart;\r\n Rebase memory _totalBorrow = totalBorrow;\r\n Rebase memory bentoBoxTotals = bentoBox.totals(collateral);\r\n for (uint256 i = 0; i < users.length; i++) {\r\n address user = users[i];\r\n if (!_isSolvent(user, _exchangeRate)) {\r\n uint256 borrowPart;\r\n {\r\n uint256 availableBorrowPart = userBorrowPart[user];\r\n borrowPart = maxBorrowParts[i] > availableBorrowPart ? availableBorrowPart : maxBorrowParts[i];\r\n userBorrowPart[user] = availableBorrowPart.sub(borrowPart);\r\n }\r\n uint256 borrowAmount = _totalBorrow.toElastic(borrowPart, false);\r\n uint256 collateralShare =\r\n bentoBoxTotals.toBase(\r\n borrowAmount.mul(LIQUIDATION_MULTIPLIER).mul(_exchangeRate) /\r\n (LIQUIDATION_MULTIPLIER_PRECISION * EXCHANGE_RATE_PRECISION),\r\n false\r\n );\r\n\r\n userCollateralShare[user] = userCollateralShare[user].sub(collateralShare);\r\n emit LogRemoveCollateral(user, to, collateralShare);\r\n emit LogRepay(msg.sender, user, borrowAmount, borrowPart);\r\n\r\n // Keep totals\r\n allCollateralShare = allCollateralShare.add(collateralShare);\r\n allBorrowAmount = allBorrowAmount.add(borrowAmount);\r\n allBorrowPart = allBorrowPart.add(borrowPart);\r\n }\r\n }\r\n require(allBorrowAmount != 0, \"Cauldron: all are solvent\");\r\n _totalBorrow.elastic = _totalBorrow.elastic.sub(allBorrowAmount.to128());\r\n _totalBorrow.base = _totalBorrow.base.sub(allBorrowPart.to128());\r\n totalBorrow = _totalBorrow;\r\n totalCollateralShare = totalCollateralShare.sub(allCollateralShare);\r\n\r\n // Apply a percentual fee share to sSpell holders\r\n \r\n {\r\n uint256 distributionAmount = (allBorrowAmount.mul(LIQUIDATION_MULTIPLIER) / LIQUIDATION_MULTIPLIER_PRECISION).sub(allBorrowAmount).mul(DISTRIBUTION_PART) / DISTRIBUTION_PRECISION; // Distribution Amount\r\n allBorrowAmount = allBorrowAmount.add(distributionAmount);\r\n accrueInfo.feesEarned = accrueInfo.feesEarned.add(distributionAmount.to128());\r\n }\r\n\r\n uint256 allBorrowShare = bentoBox.toShare(magicInternetMoney, allBorrowAmount, true);\r\n\r\n // Swap using a swapper freely chosen by the caller\r\n // Open (flash) liquidation: get proceeds first and provide the borrow after\r\n bentoBox.transfer(collateral, address(this), to, allCollateralShare);\r\n if (swapper != ISwapper(0)) {\r\n swapper.swap(collateral, magicInternetMoney, msg.sender, allBorrowShare, allCollateralShare);\r\n }\r\n\r\n bentoBox.transfer(magicInternetMoney, msg.sender, address(this), allBorrowShare);\r\n }\r\n\r\n /// @notice Withdraws the fees accumulated.\r\n function withdrawFees() public {\r\n accrue();\r\n address _feeTo = masterContract.feeTo();\r\n uint256 _feesEarned = accrueInfo.feesEarned;\r\n uint256 share = bentoBox.toShare(magicInternetMoney, _feesEarned, false);\r\n bentoBox.transfer(magicInternetMoney, address(this), _feeTo, share);\r\n accrueInfo.feesEarned = 0;\r\n\r\n emit LogWithdrawFees(_feeTo, _feesEarned);\r\n }\r\n\r\n /// @notice Sets the beneficiary of interest accrued.\r\n /// MasterContract Only Admin function.\r\n /// @param newFeeTo The address of the receiver.\r\n function setFeeTo(address newFeeTo) public onlyOwner {\r\n feeTo = newFeeTo;\r\n emit LogFeeTo(newFeeTo);\r\n }\r\n\r\n /// @notice reduces the supply of MIM\r\n /// @param amount amount to reduce supply by\r\n function reduceSupply(uint256 amount) public {\r\n require(msg.sender == masterContract.owner(), \"Caller is not the owner\");\r\n bentoBox.withdraw(magicInternetMoney, address(this), address(this), amount, 0);\r\n MagicInternetMoney(address(magicInternetMoney)).burn(amount);\r\n }\r\n}\r\n" + }, + "contracts/mocks/LendingClubMock.sol": { + "content": "// SPDX-License-Identifier: MIT\n\npragma solidity 0.6.12;\npragma experimental ABIEncoderV2;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"../NFTPair.sol\";\n\n// Minimal implementation to set up some tests.\ncontract LendingClubMock {\n INFTPair private immutable nftPair;\n address private immutable investor;\n\n constructor(INFTPair _nftPair, address _investor) public {\n nftPair = _nftPair;\n investor = _investor;\n }\n\n function init() public {\n nftPair.bentoBox().setMasterContractApproval(address(this), address(nftPair.masterContract()), true, 0, bytes32(0), bytes32(0));\n }\n\n function willLend(uint256 tokenId, TokenLoanParams memory requested) external view returns (bool) {\n if (msg.sender != address(nftPair)) {\n return false;\n }\n TokenLoanParams memory accepted = _lendingConditions(tokenId);\n // Valuation has to be an exact match, everything else must be at least\n // as good for the lender as `accepted`.\n\n return\n requested.valuation == accepted.valuation &&\n requested.duration <= accepted.duration &&\n requested.annualInterestBPS >= accepted.annualInterestBPS;\n }\n\n function _lendingConditions(uint256 tokenId) private pure returns (TokenLoanParams memory) {\n TokenLoanParams memory conditions;\n // No specific conditions given, but we'll take all even-numbered\n // ones at 100% APY:\n if (tokenId % 2 == 0) {\n // 256-bit addition fits by the above check.\n // Cast is.. relatively safe: this is a mock implementation,\n // production use is unlikely to follow this pattern for valuing\n // loans, and manipulating the token ID can only break the logic by\n // making the loan \"safer\" for the lender.\n conditions.valuation = uint128((tokenId + 1) * 10**18);\n conditions.duration = 365 days;\n conditions.annualInterestBPS = 10_000;\n }\n return conditions;\n }\n\n function lendingConditions(address _nftPair, uint256 tokenId) external view returns (TokenLoanParams memory) {\n if (_nftPair != address(nftPair)) {\n TokenLoanParams memory empty;\n return empty;\n } else {\n return _lendingConditions(tokenId);\n }\n }\n\n function seizeCollateral(uint256 tokenId) external {\n nftPair.removeCollateral(tokenId, investor);\n }\n\n function withdrawFunds(uint256 bentoShares) external {\n nftPair.bentoBox().transfer(nftPair.asset(), address(this), investor, bentoShares);\n }\n}\n" + }, + "contracts/mocks/SimpleStrategyMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@sushiswap/bentobox-sdk/contracts/IStrategy.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\n\n// solhint-disable not-rely-on-time\n\ncontract SimpleStrategyMock is IStrategy {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n IERC20 private immutable token;\n address private immutable bentoBox;\n\n modifier onlyBentoBox() {\n require(msg.sender == bentoBox, \"Ownable: caller is not the owner\");\n _;\n }\n\n constructor(address bentoBox_, IERC20 token_) public {\n bentoBox = bentoBox_;\n token = token_;\n }\n\n // Send the assets to the Strategy and call skim to invest them\n function skim(uint256) external override onlyBentoBox {\n // Leave the tokens on the contract\n return;\n }\n\n // Harvest any profits made converted to the asset and pass them to the caller\n function harvest(uint256 balance, address) external override onlyBentoBox returns (int256 amountAdded) {\n amountAdded = int256(token.balanceOf(address(this)).sub(balance));\n token.safeTransfer(bentoBox, uint256(amountAdded)); // Add as profit\n }\n\n // Withdraw assets. The returned amount can differ from the requested amount due to rounding or if the request was more than there is.\n function withdraw(uint256 amount) external override onlyBentoBox returns (uint256 actualAmount) {\n token.safeTransfer(bentoBox, uint256(amount)); // Add as profit\n actualAmount = amount;\n }\n\n // Withdraw all assets in the safest way possible. This shouldn't fail.\n function exit(uint256 balance) external override onlyBentoBox returns (int256 amountAdded) {\n amountAdded = 0;\n token.safeTransfer(bentoBox, balance);\n }\n}\n" + }, + "contracts/helpers/YearnLiquidityMigrationHelper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface IYearnVault {\n function deposit(uint256 amount, address recipient) external returns (uint256 shares);\n}\n\ncontract YearnLiquidityMigrationHelper {\n using BoringMath for uint256;\n using BoringERC20 for IERC20;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n\n constructor(IBentoBoxV1 bentoBox_) public {\n bentoBox = bentoBox_;\n }\n\n function migrate(\n IERC20 token,\n IYearnVault vault,\n uint256 amount,\n address recipient\n ) external {\n token.approve(address(vault), amount);\n uint256 shares = vault.deposit(amount, address(bentoBox));\n bentoBox.deposit(token, address(bentoBox), recipient, shares, 0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVIBSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, int128 i, uint256 min_amount, bool use_underlying) external returns(uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ncontract YVIBSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n \n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public IronBank = CurvePool(0x2dded6Da1BF5DBdF597C45fcFaa3194e53EcfeAF);\n YearnVault constant public YVIB = YearnVault(0x27b7b1ad7288079A66d12350c828D3C00A6F07d7);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n uint256 amountFrom = YVIB.withdraw();\n\n IronBank.remove_liquidity_one_coin(amountFrom, 2, 0, true);\n\n uint256 amountIntermediate = TETHER.balanceOf(address(this));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/YVCrvStETHSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, int128 i, uint256 min_amount) external returns(uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ninterface IWETH is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n function deposit() external payable;\n}\n\ncontract YVCrvStETHSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n IWETH public constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public STETH = CurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);\n YearnVault constant public YVSTETH = YearnVault(0xdCD90C7f6324cfa40d7169ef80b12031770B4325);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n receive() external payable {}\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n {\n\n uint256 amountFrom = YVSTETH.withdraw();\n\n STETH.remove_liquidity_one_coin(amountFrom, 0, 0);\n\n }\n\n uint256 amountSecond = address(this).balance;\n\n WETH.deposit{value: amountSecond}();\n WETH.transfer(address(pair), amountSecond);\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n uint256 amountIntermediate = getAmountOut(amountSecond, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/XSushiSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface SushiBar {\n function leave(uint256 share) external;\n}\n\ninterface Sushi is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVXSushiSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n SushiBar public constant xSushi = SushiBar(0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272);\n Sushi public constant SUSHI = Sushi(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2);\n IUniswapV2Pair constant SUSHI_WETH = IUniswapV2Pair(0x795065dCc9f64b5614C407a6EFDC400DA6221FB0);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n {\n\n (uint256 amountXSushiFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n xSushi.leave(amountXSushiFrom);\n\n }\n uint256 amountFirst;\n\n {\n\n uint256 amountFrom = SUSHI.balanceOf(address(this));\n\n SUSHI.transfer(address(SUSHI_WETH), amountFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = SUSHI_WETH.getReserves();\n \n amountFirst = getAmountOut(amountFrom, reserve0, reserve1);\n \n }\n\n SUSHI_WETH.swap(0, amountFirst, address(pair), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFirst, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/wOHMSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface IWOHM {\n function wrap( uint _amount ) external returns ( uint );\n function unwrap( uint _amount ) external returns ( uint );\n}\n\ninterface IOHM is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface IStakingManager {\n function unstake( uint _amount, bool _trigger ) external;\n function stake( uint _amount, address _recipient ) external returns ( bool );\n}\n\ncontract wOHMSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); \n IUniswapV2Pair constant OHM_DAI = IUniswapV2Pair(0x34d7d7Aaf50AD4944B70B320aCB24C95fa2def7c);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant SOHM = IERC20(0x04F2694C8fcee23e8Fd0dfEA1d4f5Bb8c352111F);\n IWOHM public constant WOHM = IWOHM(0xCa76543Cf381ebBB277bE79574059e32108e3E65);\n IStakingManager public constant STAKING_MANAGER = IStakingManager(0xFd31c7d00Ca47653c6Ce64Af53c1571f9C36566a);\n IOHM public constant OHM = IOHM(0x383518188C0C6d7730D91b2c03a03C837814a899);\n\n constructor(\n ) public {\n DAI.approve(address(MIM3POOL), type(uint256).max);\n SOHM.approve(address(STAKING_MANAGER), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n \n uint256 amountFirst;\n\n {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n amountFirst = WOHM.unwrap(amountFrom);\n\n }\n\n STAKING_MANAGER.unstake(amountFirst, false);\n\n OHM.transfer(address(OHM_DAI), amountFirst);\n\n (uint256 reserve0, uint256 reserve1, ) = OHM_DAI.getReserves();\n \n uint256 amountDAI = getAmountOut(amountFirst, reserve0, reserve1);\n\n OHM_DAI.swap(0, amountDAI, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(1, 0, amountDAI, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/wMEMOSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"../../libraries/UniswapV2Library.sol\";\n\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface IWMEMO is IERC20 {\n function wrap( uint _amount ) external returns ( uint );\n function unwrap( uint _amount ) external returns ( uint );\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface ITIME is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface IStakingManager {\n function unstake( uint _amount, bool _trigger ) external;\n function stake( uint _amount, address _recipient ) external returns ( bool );\n}\n\ncontract wMEMOSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xf4F46382C2bE1603Dc817551Ff9A7b333Ed1D18f);\n IUniswapV2Pair constant WMEMO_MIM = IUniswapV2Pair(0x4d308C46EA9f234ea515cC51F16fba776451cac8);\n IERC20 public constant MIM = IERC20(0x130966628846BFd36ff31a822705796e8cb8C18D);\n IERC20 public constant MEMO = IERC20(0x136Acd46C134E8269052c62A67042D6bDeDde3C9);\n IWMEMO public constant WMEMO = IWMEMO(0x0da67235dD5787D67955420C84ca1cEcd4E5Bb3b);\n IStakingManager public constant STAKING_MANAGER = IStakingManager(0x4456B87Af11e87E329AB7d7C7A246ed1aC2168B9);\n ITIME public constant TIME = ITIME(0xb54f16fB19478766A268F172C9480f8da1a7c9C3);\n address private constant WAVAX = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7;\n\n constructor(\n ) public {\n MEMO.approve(address(STAKING_MANAGER), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n \n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n\n (address token0, ) = UniswapV2Library.sortTokens(address(WMEMO), address(WMEMO_MIM));\n\n (uint256 reserve0, uint256 reserve1, ) = WMEMO_MIM.getReserves();\n\n (reserve0, reserve1) = address(WMEMO) == token0 ? (reserve0, reserve1) : (reserve1, reserve0);\n \n uint256 amountTo = getAmountOut(amountFrom, reserve0, reserve1);\n\n (uint256 amount0Out, uint256 amount1Out) = address(WMEMO) == token0\n ? (uint256(0), amountTo)\n : (amountTo, uint256(0));\n\n WMEMO_MIM.swap(amount0Out, amount1Out, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/ArbEthSwapper.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"../../libraries/UniswapV2Library.sol\";\n\n\ncontract ArbEthSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0x74c764D41B77DBbb4fe771daB1939B00b146894A);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0xb6DD51D5425861C808Fd60827Ab6CFBfFE604959);\n IERC20 constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);\n IERC20 public constant MIM = IERC20(0xFEa7a6a0B346362BF88A9e4A88416B77a57D6c2A);\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom,) = bentoBox.withdraw(fromToken, address(this), address(pair), 0, shareFrom);\n\n (address token0, ) = UniswapV2Library.sortTokens(address(MIM), address(WETH));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n\n (reserve0, reserve1) = address(WETH) == token0 ? (reserve0, reserve1) : (reserve1, reserve0);\n \n uint256 amountTo = getAmountOut(amountFrom, reserve0, reserve1);\n\n (uint256 amount0Out, uint256 amount1Out) = address(WETH) == token0\n ? (uint256(0), amountTo)\n : (amountTo, uint256(0));\n\n pair.swap(amount0Out, amount1Out, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Leverage/wMemoLevSwapper.sol": { + "content": "\n// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"../../libraries/UniswapV2Library.sol\";\n\ninterface IWMEMO is IERC20 {\n function wrap( uint _amount ) external returns ( uint );\n function unwrap( uint _amount ) external returns ( uint );\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface IStakingManager {\n function unstake( uint _amount, bool _trigger ) external;\n function stake( uint _amount, address _recipient ) external returns ( bool );\n function claim ( address _recipient ) external;\n}\n\ncontract wMEMOLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xf4F46382C2bE1603Dc817551Ff9A7b333Ed1D18f);\n IUniswapV2Pair constant WMEMO_MIM = IUniswapV2Pair(0x4d308C46EA9f234ea515cC51F16fba776451cac8);\n IERC20 public constant MIM = IERC20(0x130966628846BFd36ff31a822705796e8cb8C18D);\n IERC20 public constant MEMO = IERC20(0x136Acd46C134E8269052c62A67042D6bDeDde3C9);\n IWMEMO public constant WMEMO = IWMEMO(0x0da67235dD5787D67955420C84ca1cEcd4E5Bb3b);\n IStakingManager public constant STAKING_MANAGER = IStakingManager(0x4456B87Af11e87E329AB7d7C7A246ed1aC2168B9);\n IERC20 public constant TIME = IERC20(0xb54f16fB19478766A268F172C9480f8da1a7c9C3);\n address private constant WAVAX = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7;\n constructor(\n ) public {\n TIME.approve(address(STAKING_MANAGER), type(uint256).max);\n MEMO.approve(address(WMEMO), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountMIMFrom, ) = bentoBox.withdraw(MIM, address(this), address(WMEMO_MIM), 0, shareFrom);\n\n (address token0, ) = UniswapV2Library.sortTokens(address(WMEMO), address(MIM));\n\n (uint256 reserve0, uint256 reserve1, ) = WMEMO_MIM.getReserves();\n\n (reserve0, reserve1) = address(MIM) == token0 ? (reserve0, reserve1) : (reserve1, reserve0);\n \n uint256 amountTo = getAmountOut(amountMIMFrom, reserve0, reserve1);\n\n (uint256 amount0Out, uint256 amount1Out) = address(MIM) == token0\n ? (uint256(0), amountTo)\n : (amountTo, uint256(0));\n\n WMEMO_MIM.swap(amount0Out, amount1Out, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(WMEMO, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n}" + }, + "contracts/swappers/Leverage/ArbEthLevSwapper.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\nimport \"../../libraries/UniswapV2Library.sol\";\n\ncontract ArbEthLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0x74c764D41B77DBbb4fe771daB1939B00b146894A);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0xb6DD51D5425861C808Fd60827Ab6CFBfFE604959);\n IERC20 constant WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);\n IERC20 public constant MIM = IERC20(0xFEa7a6a0B346362BF88A9e4A88416B77a57D6c2A);\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(pair), 0, shareFrom);\n\n (address token0, ) = UniswapV2Library.sortTokens(address(MIM), address(WETH));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n\n (reserve0, reserve1) = address(MIM) == token0 ? (reserve0, reserve1) : (reserve1, reserve0);\n \n uint256 amountTo = getAmountOut(amountFrom, reserve0, reserve1);\n\n (uint256 amount0Out, uint256 amount1Out) = address(MIM) == token0\n ? (uint256(0), amountTo)\n : (amountTo, uint256(0));\n\n pair.swap(amount0Out, amount1Out, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(WETH, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/UniswapV2ERC20.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\nimport './libraries/SafeMath.sol';\n\ncontract UniswapV2ERC20 {\n using SafeMathUniswap for uint;\n\n string public constant name = 'SushiSwap LP Token';\n string public constant symbol = 'SLP';\n uint8 public constant decimals = 18;\n uint public totalSupply;\n mapping(address => uint) public balanceOf;\n mapping(address => mapping(address => uint)) public allowance;\n\n bytes32 public DOMAIN_SEPARATOR;\n // keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;\n mapping(address => uint) public nonces;\n\n event Approval(address indexed owner, address indexed spender, uint value);\n event Transfer(address indexed from, address indexed to, uint value);\n\n constructor() public {\n uint chainId;\n assembly {\n chainId := chainid()\n }\n DOMAIN_SEPARATOR = keccak256(\n abi.encode(\n keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),\n keccak256(bytes(name)),\n keccak256(bytes('1')),\n chainId,\n address(this)\n )\n );\n }\n\n function _mint(address to, uint value) internal {\n totalSupply = totalSupply.add(value);\n balanceOf[to] = balanceOf[to].add(value);\n emit Transfer(address(0), to, value);\n }\n\n function _burn(address from, uint value) internal {\n balanceOf[from] = balanceOf[from].sub(value);\n totalSupply = totalSupply.sub(value);\n emit Transfer(from, address(0), value);\n }\n\n function _approve(address owner, address spender, uint value) private {\n allowance[owner][spender] = value;\n emit Approval(owner, spender, value);\n }\n\n function _transfer(address from, address to, uint value) private {\n balanceOf[from] = balanceOf[from].sub(value);\n balanceOf[to] = balanceOf[to].add(value);\n emit Transfer(from, to, value);\n }\n\n function approve(address spender, uint value) external returns (bool) {\n _approve(msg.sender, spender, value);\n return true;\n }\n\n function transfer(address to, uint value) external returns (bool) {\n _transfer(msg.sender, to, value);\n return true;\n }\n\n function transferFrom(address from, address to, uint value) external returns (bool) {\n if (allowance[from][msg.sender] != uint(-1)) {\n allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);\n }\n _transfer(from, to, value);\n return true;\n }\n\n function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {\n require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');\n bytes32 digest = keccak256(\n abi.encodePacked(\n '\\x19\\x01',\n DOMAIN_SEPARATOR,\n keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))\n )\n );\n address recoveredAddress = ecrecover(digest, v, r, s);\n require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');\n _approve(owner, spender, value);\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/UniswapV2Pair.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\nimport './UniswapV2ERC20.sol';\nimport './libraries/Math.sol';\nimport './libraries/UQ112x112.sol';\nimport './interfaces/IERC20.sol';\nimport './interfaces/IUniswapV2Factory.sol';\nimport './interfaces/IUniswapV2Callee.sol';\n\ninterface IMigrator {\n // Return the desired amount of liquidity token that the migrator wants.\n function desiredLiquidity() external view returns (uint256);\n}\n\ncontract UniswapV2Pair is UniswapV2ERC20 {\n using SafeMathUniswap for uint;\n using UQ112x112 for uint224;\n\n uint public constant MINIMUM_LIQUIDITY = 10**3;\n bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));\n\n address public factory;\n address public token0;\n address public token1;\n\n uint112 private reserve0; // uses single storage slot, accessible via getReserves\n uint112 private reserve1; // uses single storage slot, accessible via getReserves\n uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves\n\n uint public price0CumulativeLast;\n uint public price1CumulativeLast;\n uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event\n\n uint private unlocked = 1;\n modifier lock() {\n require(unlocked == 1, 'UniswapV2: LOCKED');\n unlocked = 0;\n _;\n unlocked = 1;\n }\n\n function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {\n _reserve0 = reserve0;\n _reserve1 = reserve1;\n _blockTimestampLast = blockTimestampLast;\n }\n\n function _safeTransfer(address token, address to, uint value) private {\n (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));\n require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');\n }\n\n event Mint(address indexed sender, uint amount0, uint amount1);\n event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);\n event Swap(\n address indexed sender,\n uint amount0In,\n uint amount1In,\n uint amount0Out,\n uint amount1Out,\n address indexed to\n );\n event Sync(uint112 reserve0, uint112 reserve1);\n\n constructor() public {\n factory = msg.sender;\n }\n\n // called once by the factory at time of deployment\n function initialize(address _token0, address _token1) external {\n require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check\n token0 = _token0;\n token1 = _token1;\n }\n\n // update reserves and, on the first call per block, price accumulators\n function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {\n require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');\n uint32 blockTimestamp = uint32(block.timestamp % 2**32);\n uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired\n if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {\n // * never overflows, and + overflow is desired\n price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;\n price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;\n }\n reserve0 = uint112(balance0);\n reserve1 = uint112(balance1);\n blockTimestampLast = blockTimestamp;\n emit Sync(reserve0, reserve1);\n }\n\n // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)\n function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {\n address feeTo = IUniswapV2Factory(factory).feeTo();\n feeOn = feeTo != address(0);\n uint _kLast = kLast; // gas savings\n if (feeOn) {\n if (_kLast != 0) {\n uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));\n uint rootKLast = Math.sqrt(_kLast);\n if (rootK > rootKLast) {\n uint numerator = totalSupply.mul(rootK.sub(rootKLast));\n uint denominator = rootK.mul(5).add(rootKLast);\n uint liquidity = numerator / denominator;\n if (liquidity > 0) _mint(feeTo, liquidity);\n }\n }\n } else if (_kLast != 0) {\n kLast = 0;\n }\n }\n\n // this low-level function should be called from a contract which performs important safety checks\n function mint(address to) external lock returns (uint liquidity) {\n (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings\n uint balance0 = IERC20Uniswap(token0).balanceOf(address(this));\n uint balance1 = IERC20Uniswap(token1).balanceOf(address(this));\n uint amount0 = balance0.sub(_reserve0);\n uint amount1 = balance1.sub(_reserve1);\n\n bool feeOn = _mintFee(_reserve0, _reserve1);\n uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee\n if (_totalSupply == 0) {\n address migrator = IUniswapV2Factory(factory).migrator();\n if (msg.sender == migrator) {\n liquidity = IMigrator(migrator).desiredLiquidity();\n require(liquidity > 0 && liquidity != uint256(-1), \"Bad desired liquidity\");\n } else {\n require(migrator == address(0), \"Must not have migrator\");\n liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);\n _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens\n }\n } else {\n liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);\n }\n require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');\n _mint(to, liquidity);\n\n _update(balance0, balance1, _reserve0, _reserve1);\n if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date\n emit Mint(msg.sender, amount0, amount1);\n }\n\n // this low-level function should be called from a contract which performs important safety checks\n function burn(address to) external lock returns (uint amount0, uint amount1) {\n (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings\n address _token0 = token0; // gas savings\n address _token1 = token1; // gas savings\n uint balance0 = IERC20Uniswap(_token0).balanceOf(address(this));\n uint balance1 = IERC20Uniswap(_token1).balanceOf(address(this));\n uint liquidity = balanceOf[address(this)];\n\n bool feeOn = _mintFee(_reserve0, _reserve1);\n uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee\n amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution\n amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution\n require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');\n _burn(address(this), liquidity);\n _safeTransfer(_token0, to, amount0);\n _safeTransfer(_token1, to, amount1);\n balance0 = IERC20Uniswap(_token0).balanceOf(address(this));\n balance1 = IERC20Uniswap(_token1).balanceOf(address(this));\n\n _update(balance0, balance1, _reserve0, _reserve1);\n if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date\n emit Burn(msg.sender, amount0, amount1, to);\n }\n\n // this low-level function should be called from a contract which performs important safety checks\n function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {\n require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');\n (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings\n require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');\n\n uint balance0;\n uint balance1;\n { // scope for _token{0,1}, avoids stack too deep errors\n address _token0 = token0;\n address _token1 = token1;\n require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');\n if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens\n if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens\n if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);\n balance0 = IERC20Uniswap(_token0).balanceOf(address(this));\n balance1 = IERC20Uniswap(_token1).balanceOf(address(this));\n }\n uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;\n uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;\n require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');\n { // scope for reserve{0,1}Adjusted, avoids stack too deep errors\n uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));\n uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));\n require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');\n }\n\n _update(balance0, balance1, _reserve0, _reserve1);\n emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);\n }\n\n // force balances to match reserves\n function skim(address to) external lock {\n address _token0 = token0; // gas savings\n address _token1 = token1; // gas savings\n _safeTransfer(_token0, to, IERC20Uniswap(_token0).balanceOf(address(this)).sub(reserve0));\n _safeTransfer(_token1, to, IERC20Uniswap(_token1).balanceOf(address(this)).sub(reserve1));\n }\n\n // force reserves to match balances\n function sync() external lock {\n _update(IERC20Uniswap(token0).balanceOf(address(this)), IERC20Uniswap(token1).balanceOf(address(this)), reserve0, reserve1);\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/libraries/Math.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\n// a library for performing various math operations\n\nlibrary Math {\n function min(uint x, uint y) internal pure returns (uint z) {\n z = x < y ? x : y;\n }\n\n // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)\n function sqrt(uint y) internal pure returns (uint z) {\n if (y > 3) {\n z = y;\n uint x = y / 2 + 1;\n while (x < z) {\n z = x;\n x = (y / x + x) / 2;\n }\n } else if (y != 0) {\n z = 1;\n }\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/libraries/UQ112x112.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\n// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))\n\n// range: [0, 2**112 - 1]\n// resolution: 1 / 2**112\n\nlibrary UQ112x112 {\n uint224 constant Q112 = 2**112;\n\n // encode a uint112 as a UQ112x112\n function encode(uint112 y) internal pure returns (uint224 z) {\n z = uint224(y) * Q112; // never overflows\n }\n\n // divide a UQ112x112 by a uint112, returning a UQ112x112\n function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {\n z = x / uint224(y);\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/interfaces/IERC20.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.5.0;\n\ninterface IERC20Uniswap {\n event Approval(address indexed owner, address indexed spender, uint value);\n event Transfer(address indexed from, address indexed to, uint value);\n\n function name() external view returns (string memory);\n function symbol() external view returns (string memory);\n function decimals() external view returns (uint8);\n function totalSupply() external view returns (uint);\n function balanceOf(address owner) external view returns (uint);\n function allowance(address owner, address spender) external view returns (uint);\n\n function approve(address spender, uint value) external returns (bool);\n function transfer(address to, uint value) external returns (bool);\n function transferFrom(address from, address to, uint value) external returns (bool);\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Callee.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity >=0.5.0;\n\ninterface IUniswapV2Callee {\n function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;\n}\n" + }, + "contracts/mocks/SushiSwapPairMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@sushiswap/core/contracts/uniswapv2/UniswapV2Pair.sol\";\n\ncontract SushiSwapPairMock is UniswapV2Pair {\n constructor() public UniswapV2Pair() {\n return;\n }\n}\n" + }, + "@sushiswap/core/contracts/uniswapv2/UniswapV2Factory.sol": { + "content": "// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.6.12;\n\nimport './interfaces/IUniswapV2Factory.sol';\nimport './UniswapV2Pair.sol';\n\ncontract UniswapV2Factory is IUniswapV2Factory {\n address public override feeTo;\n address public override feeToSetter;\n address public override migrator;\n\n mapping(address => mapping(address => address)) public override getPair;\n address[] public override allPairs;\n\n event PairCreated(address indexed token0, address indexed token1, address pair, uint);\n\n constructor(address _feeToSetter) public {\n feeToSetter = _feeToSetter;\n }\n\n function allPairsLength() external override view returns (uint) {\n return allPairs.length;\n }\n\n function pairCodeHash() external pure returns (bytes32) {\n return keccak256(type(UniswapV2Pair).creationCode);\n }\n\n function createPair(address tokenA, address tokenB) external override returns (address pair) {\n require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');\n (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);\n require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');\n require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient\n bytes memory bytecode = type(UniswapV2Pair).creationCode;\n bytes32 salt = keccak256(abi.encodePacked(token0, token1));\n assembly {\n pair := create2(0, add(bytecode, 32), mload(bytecode), salt)\n }\n UniswapV2Pair(pair).initialize(token0, token1);\n getPair[token0][token1] = pair;\n getPair[token1][token0] = pair; // populate mapping in the reverse direction\n allPairs.push(pair);\n emit PairCreated(token0, token1, pair, allPairs.length);\n }\n\n function setFeeTo(address _feeTo) external override {\n require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');\n feeTo = _feeTo;\n }\n\n function setMigrator(address _migrator) external override {\n require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');\n migrator = _migrator;\n }\n\n function setFeeToSetter(address _feeToSetter) external override {\n require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');\n feeToSetter = _feeToSetter;\n }\n\n}\n" + }, + "contracts/mocks/SushiSwapFactoryMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/UniswapV2Factory.sol\";\n\ncontract SushiSwapFactoryMock is UniswapV2Factory {\n constructor() public UniswapV2Factory(msg.sender) {\n return;\n }\n}\n" + }, + "contracts/swappers/Liquidations/WethSwapper.sol": { + "content": "pragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, int128 i, uint256 min_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n}\n\ncontract WethSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant degenBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n WETH.approve(address(threecrypto), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = degenBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n threecrypto.exchange(2, 0, amountFrom, 0);\n\n uint256 amountIntermediate = TETHER.balanceOf(address(this));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(degenBox));\n\n (, shareReturned) = degenBox.deposit(toToken, address(degenBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/WbtcSwapper.sol": { + "content": "pragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, int128 i, uint256 min_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n}\n\ncontract WbtcSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant degenBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n WBTC.approve(address(threecrypto), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = degenBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n threecrypto.exchange(1, 0, amountFrom, 0);\n\n uint256 amountIntermediate = TETHER.balanceOf(address(this));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(degenBox));\n\n (, shareReturned) = degenBox.deposit(toToken, address(degenBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/ThreeCryptoSwapper.sol": { + "content": "pragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, uint256 i, uint256 min_amount) external;\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n}\n\ncontract ThreeCryptoSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n \n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n CurvePool constant public threecrypto = CurvePool(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n IConvex public constant cvx3Crypto = IConvex(0x5958A8DB7dfE0CC49382209069b00F54e17929C2);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n cvx3Crypto.withdrawAndUnwrap(amountFrom);\n\n threecrypto.remove_liquidity_one_coin(amountFrom, 0, 0);\n\n uint256 amountIntermediate = TETHER.balanceOf(address(this));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/sSpellSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface IsSpell {\n function burn(address to, uint256 shares) external returns (bool);\n}\n\ninterface ISpell is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract SSpellSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IsSpell public constant sSpell = IsSpell(0x26FA3fFFB6EfE8c1E69103aCb4044C26B9A106a9);\n ISpell public constant SPELL = ISpell(0x090185f2135308BaD17527004364eBcC2D37e5F6);\n IUniswapV2Pair constant SPELL_ETH = IUniswapV2Pair(0xb5De0C3753b6E1B4dBA616Db82767F17513E6d4E);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor(\n ) public {\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n {\n\n (uint256 amountSSpellFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n sSpell.burn(address(this), amountSSpellFrom);\n\n }\n uint256 amountFirst;\n\n {\n\n uint256 amountFrom = SPELL.balanceOf(address(this));\n\n SPELL.transfer(address(SPELL_ETH), amountFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = SPELL_ETH.getReserves();\n \n amountFirst = getAmountOut(amountFrom, reserve0, reserve1);\n \n }\n\n SPELL_ETH.swap(0, amountFirst, address(pair), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFirst, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/SpellSuperSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface IsSpell {\n function burn(address to, uint256 shares) external returns (bool);\n}\n\ninterface ISpell is IERC20 {\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract SpellSuperSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IsSpell public constant sSpell = IsSpell(0x26FA3fFFB6EfE8c1E69103aCb4044C26B9A106a9);\n ISpell public constant SPELL = ISpell(0x090185f2135308BaD17527004364eBcC2D37e5F6);\n IUniswapV2Pair constant SPELL_ETH = IUniswapV2Pair(0xb5De0C3753b6E1B4dBA616Db82767F17513E6d4E);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor(\n ) public {\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(SPELL_ETH), 0, shareFrom);\n\n uint256 amountFirst;\n\n {\n\n (uint256 reserve0, uint256 reserve1, ) = SPELL_ETH.getReserves();\n \n amountFirst = getAmountOut(amountFrom, reserve0, reserve1);\n \n }\n\n SPELL_ETH.swap(0, amountFirst, address(pair), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFirst, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/RenCrvSwapper.sol": { + "content": "pragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n function approve(address _spender, uint256 _value) external returns (bool);\n function remove_liquidity_one_coin(uint256 tokenAmount, int128 i, uint256 min_amount) external;\n}\n\ninterface IThreeCrypto is CurvePool {\n function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external;\n}\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n function balanceOf(address user) external view returns (uint256);\n}\n\ninterface IConvex is IERC20{\n function withdrawAndUnwrap(uint256 _amount) external;\n}\n\ncontract RenCrvSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool constant public renCrv = CurvePool(0x93054188d876f558f4a66B2EF1d97d16eDf0895B);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IThreeCrypto constant public threecrypto = IThreeCrypto(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46);\n IConvex public constant cvxRen = IConvex(0xB65eDE134521F0EFD4E943c835F450137dC6E83e);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant CurveToken = IERC20(0x49849C98ae39Fff122806C06791Fa73784FB3675);\n IERC20 public constant WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n WBTC.approve(address(threecrypto), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(this), 0, shareFrom);\n\n cvxRen.withdrawAndUnwrap(amountFrom);\n\n renCrv.remove_liquidity_one_coin(amountFrom, 1, 0);\n\n uint256 amountOne = WBTC.balanceOf(address(this));\n\n threecrypto.exchange(1, 0, amountOne, 0);\n\n uint256 amountIntermediate = TETHER.balanceOf(address(this));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}" + }, + "contracts/swappers/Liquidations/FTMSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\n\ncontract FTMSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n \n IUniswapV2Pair constant pair = IUniswapV2Pair(0xB32b31DfAfbD53E310390F641C7119b5B9Ea0488);\n IERC20 constant WFTM = IERC20(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);\n IERC20 public constant MIM = IERC20(0x82f0B8B456c1A451378467398982d4834b6829c1);\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom,) = bentoBox.withdraw(fromToken, address(this), address(pair), 0, shareFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountTo = getAmountOut(amountFrom, reserve0, reserve1);\n pair.swap(0, amountTo, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Liquidations/ALCXSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"../../interfaces/ISwapper.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract ALCXSwapper is ISwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant ALCX = IERC20(0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF);\n IUniswapV2Pair constant ALCX_WETH = IUniswapV2Pair(0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n\n constructor() public {\n TETHER.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n// Swaps to a flexible amount, from an exact input amount\n /// @inheritdoc ISwapper\n function swap(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public override returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(fromToken, address(this), address(ALCX_WETH), 0, shareFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = ALCX_WETH.getReserves();\n \n uint256 amountFirst = getAmountOut(amountFrom, reserve1, reserve0);\n\n ALCX_WETH.swap(amountFirst, 0, address(pair), new bytes(0));\n\n (reserve0, reserve1, ) = pair.getReserves();\n \n uint256 amountIntermediate = getAmountOut(amountFirst, reserve0, reserve1);\n pair.swap(0, amountIntermediate, address(this), new bytes(0));\n\n uint256 amountTo = MIM3POOL.exchange_underlying(3, 0, amountIntermediate, 0, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(toToken, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n // Swaps to an exact amount, from a flexible input amount\n /// @inheritdoc ISwapper\n function swapExact(\n IERC20 fromToken,\n IERC20 toToken,\n address recipient,\n address refundTo,\n uint256 shareFromSupplied,\n uint256 shareToExact\n ) public override returns (uint256 shareUsed, uint256 shareReturned) {\n return (0,0);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVYFILevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVYFILevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n YearnVault public constant YFI_VAULT = YearnVault(0xE14d13d8B3b85aF791b2AADD661cDBd5E6097Db1);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7);\n IUniswapV2Pair constant YFI_WETH = IUniswapV2Pair(0x088ee5007C98a9677165D78dD2109AE4a3D04d0C);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 constant YFI = IERC20(0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n MIM.approve(address(MIM3POOL), type(uint256).max);\n YFI.approve(address(YFI_VAULT), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n uint256 amountFirst;\n\n {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n amountFirst = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(pair));\n\n }\n\n uint256 amountIntermediate;\n \n {\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n pair.swap(amountIntermediate, 0, address(YFI_WETH), new bytes(0));\n\n }\n\n (uint256 reserve0, uint256 reserve1, ) = YFI_WETH.getReserves();\n \n uint256 amountInt2 = getAmountOut(amountIntermediate, reserve1, reserve0);\n \n YFI_WETH.swap(amountInt2, 0, address(this), new bytes(0));\n\n uint256 amountTo = YFI_VAULT.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(YFI_VAULT)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/YVXSushiLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface SushiBar is IERC20 {\n function leave(uint256 share) external;\n function enter(uint256 amount) external;\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract YVXSushiLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n SushiBar public constant xSushi = SushiBar(0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272);\n IERC20 public constant SUSHI = IERC20(0x6B3595068778DD592e39A122f4f5a5cF09C90fE2);\n IUniswapV2Pair constant SUSHI_WETH = IUniswapV2Pair(0x795065dCc9f64b5614C407a6EFDC400DA6221FB0);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n SUSHI.approve(address(xSushi), type(uint256).max);\n MIM.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n uint256 amountFirst;\n uint256 amountIntermediate;\n\n {\n\n (uint256 amountMIMFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n amountFirst = MIM3POOL.exchange_underlying(0, 3, amountMIMFrom, 0, address(pair));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n\n }\n\n uint256 amountThird;\n\n {\n \n pair.swap(amountIntermediate, 0, address(SUSHI_WETH), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = SUSHI_WETH.getReserves();\n \n amountThird = getAmountOut(amountIntermediate, reserve1, reserve0);\n\n }\n\n SUSHI_WETH.swap(amountThird, 0, address(this), new bytes(0));\n\n xSushi.enter(amountThird);\n\n uint256 amountTo = xSushi.balanceOf(address(this));\n\n xSushi.transfer(address(bentoBox), amountTo);\n\n (, shareReturned) = bentoBox.deposit(xSushi, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n}\n" + }, + "contracts/swappers/Leverage/YVWETHLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface YearnVault {\n function withdraw() external returns (uint256);\n function deposit(uint256 amount, address recipient) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\ncontract YVWETHLevSwapper{\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public immutable bentoBox;\n\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n YearnVault public constant WETH_VAULT = YearnVault(0xa258C4606Ca8206D8aA700cE2143D7db854D168c);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor(\n IBentoBoxV1 bentoBox_\n ) public {\n bentoBox = bentoBox_;\n WETH.approve(address(WETH_VAULT), type(uint256).max);\n MIM.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountIntermediate = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(pair));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountInt2 = getAmountOut(amountIntermediate, reserve1, reserve0);\n pair.swap(amountInt2, 0, address(this), new bytes(0));\n\n uint256 amountTo = WETH_VAULT.deposit(type(uint256).max, address(bentoBox));\n\n (, shareReturned) = bentoBox.deposit(IERC20(address(WETH_VAULT)), address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/wOHMLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface IWOHM is IERC20 {\n function wrap( uint _amount ) external returns ( uint );\n function unwrap( uint _amount ) external returns ( uint );\n function transfer(address _to, uint256 _value) external returns (bool success);\n}\n\ninterface IStakingManager {\n function unstake( uint _amount, bool _trigger ) external;\n function stake( uint _amount, address _recipient ) external returns ( bool );\n function claim ( address _recipient ) external;\n}\n\ncontract wOHMLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n IUniswapV2Pair constant OHM_DAI = IUniswapV2Pair(0x34d7d7Aaf50AD4944B70B320aCB24C95fa2def7c);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n IERC20 public constant SOHM = IERC20(0x04F2694C8fcee23e8Fd0dfEA1d4f5Bb8c352111F);\n IWOHM public constant WOHM = IWOHM(0xCa76543Cf381ebBB277bE79574059e32108e3E65);\n IStakingManager public constant STAKING_MANAGER = IStakingManager(0xFd31c7d00Ca47653c6Ce64Af53c1571f9C36566a);\n IERC20 public constant OHM = IERC20(0x383518188C0C6d7730D91b2c03a03C837814a899);\n constructor(\n ) public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n OHM.approve(address(STAKING_MANAGER), type(uint256).max);\n SOHM.approve(address(WOHM), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n uint256 amountFirst;\n uint256 amountIntermediate;\n\n {\n\n (uint256 amountMIMFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n amountFirst = MIM3POOL.exchange_underlying(0, 1, amountMIMFrom, 0, address(OHM_DAI));\n\n (uint256 reserve0, uint256 reserve1, ) = OHM_DAI.getReserves();\n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n }\n \n OHM_DAI.swap(amountIntermediate, 0, address(this), new bytes(0));\n\n STAKING_MANAGER.stake(amountIntermediate, address(this));\n\n STAKING_MANAGER.claim(address(this));\n\n uint256 amountTo = WOHM.wrap(amountIntermediate);\n\n WOHM.transfer(address(bentoBox), amountTo);\n\n (, shareReturned) = bentoBox.deposit(WOHM, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n\n}" + }, + "contracts/swappers/Leverage/SpellLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract SpellLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant SPELL = IERC20(0x090185f2135308BaD17527004364eBcC2D37e5F6);\n IUniswapV2Pair constant SPELL_ETH = IUniswapV2Pair(0xb5De0C3753b6E1B4dBA616Db82767F17513E6d4E);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n uint256 amountFirst = MIM3POOL.exchange_underlying(0, 3, amountFrom, 0, address(pair));\n \n uint256 amountIntermediate;\n {\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n pair.swap(amountIntermediate, 0, address(SPELL_ETH), new bytes(0));\n\n }\n\n uint256 amountTo;\n\n {\n\n (uint256 reserve0, uint256 reserve1, ) = SPELL_ETH.getReserves();\n \n amountTo = getAmountOut(amountIntermediate, reserve1, reserve0);\n \n }\n\n SPELL_ETH.swap(amountTo, 0, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(SPELL, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/FTMLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ncontract FTMLevSwapper{\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0xB32b31DfAfbD53E310390F641C7119b5B9Ea0488);\n IERC20 constant WFTM = IERC20(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);\n IERC20 public constant MIM = IERC20(0x82f0B8B456c1A451378467398982d4834b6829c1);\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n (uint256 amountFrom, ) = bentoBox.withdraw(MIM, address(this), address(pair), 0, shareFrom);\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n \n uint256 amountTo = getAmountOut(amountFrom, reserve1, reserve0);\n pair.swap(amountTo, 0, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(WFTM, address(bentoBox), recipient, amountTo, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/swappers/Leverage/ALCXLevSwapper.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Factory.sol\";\nimport \"@sushiswap/core/contracts/uniswapv2/interfaces/IUniswapV2Pair.sol\";\nimport \"@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol\";\n\ninterface CurvePool {\n function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);\n}\n\ninterface TetherToken {\n function approve(address _spender, uint256 _value) external;\n}\n\ncontract ALCXLevSwapper {\n using BoringMath for uint256;\n\n // Local variables\n IBentoBoxV1 public constant bentoBox = IBentoBoxV1(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966);\n CurvePool public constant MIM3POOL = CurvePool(0x5a6A4D54456819380173272A5E8E9B9904BdF41B);\n TetherToken public constant TETHER = TetherToken(0xdAC17F958D2ee523a2206206994597C13D831ec7); \n IERC20 public constant ALCX = IERC20(0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF);\n IUniswapV2Pair constant ALCX_WETH = IUniswapV2Pair(0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8);\n IUniswapV2Pair constant pair = IUniswapV2Pair(0x06da0fd433C1A5d7a4faa01111c044910A184553);\n IERC20 public constant MIM = IERC20(0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3);\n\n constructor() public {\n MIM.approve(address(MIM3POOL), type(uint256).max);\n }\n\n // Given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset\n function getAmountOut(\n uint256 amountIn,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountOut) {\n uint256 amountInWithFee = amountIn.mul(997);\n uint256 numerator = amountInWithFee.mul(reserveOut);\n uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);\n amountOut = numerator / denominator;\n }\n\n // Given an output amount of an asset and pair reserves, returns a required input amount of the other asset\n function getAmountIn(\n uint256 amountOut,\n uint256 reserveIn,\n uint256 reserveOut\n ) internal pure returns (uint256 amountIn) {\n uint256 numerator = reserveIn.mul(amountOut).mul(1000);\n uint256 denominator = reserveOut.sub(amountOut).mul(997);\n amountIn = (numerator / denominator).add(1);\n }\n\n // Swaps to a flexible amount, from an exact input amount\n function swap(\n address recipient,\n uint256 shareToMin,\n uint256 shareFrom\n ) public returns (uint256 extraShare, uint256 shareReturned) {\n\n uint256 amountFirst;\n uint256 amountIntermediate;\n\n {\n\n (uint256 amountMIMFrom, ) = bentoBox.withdraw(MIM, address(this), address(this), 0, shareFrom);\n\n amountFirst = MIM3POOL.exchange_underlying(0, 3, amountMIMFrom, 0, address(pair));\n\n (uint256 reserve0, uint256 reserve1, ) = pair.getReserves();\n amountIntermediate = getAmountOut(amountFirst, reserve1, reserve0);\n\n }\n\n pair.swap(amountIntermediate, 0, address(ALCX_WETH), new bytes(0));\n\n (uint256 reserve0, uint256 reserve1, ) = ALCX_WETH.getReserves();\n \n uint256 amountThird = getAmountOut(amountIntermediate, reserve0, reserve1);\n\n ALCX_WETH.swap(0, amountThird, address(bentoBox), new bytes(0));\n\n (, shareReturned) = bentoBox.deposit(ALCX, address(bentoBox), recipient, amountThird, 0);\n extraShare = shareReturned.sub(shareToMin);\n }\n}\n" + }, + "contracts/mocks/FreelyMintableERC20Mock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\nimport \"./ERC20Mock.sol\";\n\ncontract FreelyMintableERC20Mock is ERC20Mock {\n using BoringMath for uint256;\n\n constructor(uint256 initialSupply) public ERC20Mock(initialSupply) {}\n\n function mint(address to, uint256 amount) public {\n totalSupply = totalSupply.add(amount);\n balanceOf[to] += amount;\n emit Transfer(address(0), to, amount);\n }\n\n function burn(uint256 amount) public {\n require(amount <= balanceOf[msg.sender], \"MIM: not enough\");\n totalSupply -= amount;\n emit Transfer(msg.sender, address(0), amount);\n }\n}\n" + }, + "contracts/mocks/ERC20Mock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/ERC20.sol\";\n\ncontract ERC20Mock is ERC20 {\n uint256 public override totalSupply;\n\n constructor(uint256 _initialAmount) public {\n // Give the creator all initial tokens\n balanceOf[msg.sender] = _initialAmount;\n // Update total supply\n totalSupply = _initialAmount;\n }\n}\n" + }, + "contracts/mocks/ExternalFunctionMock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol\";\n\ncontract ExternalFunctionMock {\n using BoringMath for uint256;\n\n event Result(uint256 output);\n\n function sum(uint256 a, uint256 b) external returns (uint256 c) {\n c = a.add(b);\n emit Result(c);\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"./libraries/BoringAddress.sol\";\nimport \"./libraries/BoringMath.sol\";\nimport \"./interfaces/IERC721TokenReceiver.sol\";\n\n// solhint-disable avoid-low-level-calls\n\nabstract contract BoringMultipleNFT {\n /// This contract is an EIP-721 compliant contract with enumerable support\n /// To optimize for gas, tokenId is sequential and start at 0. Also, tokens can't be removed/burned.\n using BoringAddress for address;\n using BoringMath for uint256;\n event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\n event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\n event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\n\n uint256 public totalSupply = 0;\n\n struct TokenInfo {\n address owner;\n uint24 index; // index in the tokensOf array, one address can hold a maximum of 16,777,216 tokens\n uint72 data; // data field can be usse to store traits\n }\n\n // operator mappings as per usual\n mapping(address => mapping(address => bool)) public isApprovedForAll;\n mapping(address => uint256[]) public tokensOf; // Array of tokens owned by\n mapping(uint256 => TokenInfo) internal _tokens; // The index in the tokensOf array for the token, needed to remove tokens from tokensOf\n mapping(uint256 => address) internal _approved; // keep track of approved nft\n\n function supportsInterface(bytes4 interfaceID) external pure returns (bool) {\n return\n interfaceID == this.supportsInterface.selector || // EIP-165\n interfaceID == 0x80ac58cd; // EIP-721\n }\n\n function approve(address approved, uint256 tokenId) public payable {\n address owner = _tokens[tokenId].owner;\n require(msg.sender == owner || isApprovedForAll[owner][msg.sender], \"Not allowed\");\n _approved[tokenId] = approved;\n emit Approval(owner, approved, tokenId);\n }\n\n function getApproved(uint256 tokenId) public view returns (address approved) {\n require(tokenId < totalSupply, \"Invalid tokenId\");\n return _approved[tokenId];\n }\n\n function setApprovalForAll(address operator, bool approved) public {\n isApprovedForAll[msg.sender][operator] = approved;\n emit ApprovalForAll(msg.sender, operator, approved);\n }\n\n function ownerOf(uint256 tokenId) public view returns (address) {\n address owner = _tokens[tokenId].owner;\n require(owner != address(0), \"No owner\");\n return owner;\n }\n\n function balanceOf(address owner) public view returns (uint256) {\n require(owner != address(0), \"No 0 owner\");\n return tokensOf[owner].length;\n }\n\n function _transferBase(\n uint256 tokenId,\n address from,\n address to,\n uint72 data\n ) internal {\n address owner = _tokens[tokenId].owner;\n require(from == owner, \"From not owner\");\n\n uint24 index;\n // Remove the token from the current owner's tokensOf array\n if (from != address(0)) {\n index = _tokens[tokenId].index; // The index of the item to remove in the array\n data = _tokens[tokenId].data;\n uint256 last = tokensOf[from].length - 1;\n uint256 lastTokenId = tokensOf[from][last];\n tokensOf[from][index] = lastTokenId; // Copy the last item into the slot of the one to be removed\n _tokens[lastTokenId].index = index; // Update the token index for the last item that was moved\n tokensOf[from].pop(); // Delete the last item\n }\n\n index = uint24(tokensOf[to].length);\n tokensOf[to].push(tokenId);\n _tokens[tokenId] = TokenInfo({owner: to, index: index, data: data});\n\n // EIP-721 seems to suggest not to emit the Approval event here as it is indicated by the Transfer event.\n _approved[tokenId] = address(0);\n emit Transfer(from, to, tokenId);\n }\n\n function _transfer(\n address from,\n address to,\n uint256 tokenId\n ) internal {\n require(msg.sender == from || msg.sender == _approved[tokenId] || isApprovedForAll[from][msg.sender], \"Transfer not allowed\");\n require(to != address(0), \"No zero address\");\n // check for owner == from is in base\n _transferBase(tokenId, from, to, 0);\n }\n\n function transferFrom(\n address from,\n address to,\n uint256 tokenId\n ) public payable {\n _transfer(from, to, tokenId);\n }\n\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId\n ) public payable {\n safeTransferFrom(from, to, tokenId, \"\");\n }\n\n function safeTransferFrom(\n address from,\n address to,\n uint256 tokenId,\n bytes memory data\n ) public payable {\n _transfer(from, to, tokenId);\n if (to.isContract()) {\n require(\n IERC721TokenReceiver(to).onERC721Received(msg.sender, from, tokenId, data) ==\n bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\")),\n \"Wrong return value\"\n );\n }\n }\n\n function tokenURI(uint256 tokenId) public view returns (string memory) {\n require(tokenId < totalSupply, \"Not minted\");\n return _tokenURI(tokenId);\n }\n\n function _tokenURI(uint256 tokenId) internal view virtual returns (string memory);\n\n function tokenByIndex(uint256 index) public view returns (uint256) {\n require(index < totalSupply, \"Out of bounds\");\n return index; // This works due the optimization of sequential tokenIds and no burning\n }\n\n function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) {\n return tokensOf[owner][index];\n }\n\n function _mint(address owner, uint72 data) internal {\n _transferBase(totalSupply, address(0), owner, data);\n totalSupply++;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/libraries/BoringAddress.sol": { + "content": "// SPDX-License-Identifier: MIT\n\n//SPDX-License-Identifier: MIT\npragma solidity ^0.6.12;\n\n// solhint-disable no-inline-assembly\n\nlibrary BoringAddress {\n function isContract(address account) internal view returns (bool) {\n uint256 size;\n assembly {\n size := extcodesize(account)\n }\n return size > 0;\n }\n}\n" + }, + "@boringcrypto/boring-solidity/contracts/interfaces/IERC721TokenReceiver.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\n\ninterface IERC721TokenReceiver {\n function onERC721Received(\n address _operator,\n address _from,\n uint256 _tokenId,\n bytes calldata _data\n ) external returns (bytes4);\n}\n" + }, + "contracts/mocks/ERC721Mock.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.6.12;\nimport \"@boringcrypto/boring-solidity/contracts/BoringMultipleNFT.sol\";\n\ncontract ERC721Mock is BoringMultipleNFT {\n function mint(address owner) public returns (uint256 id) {\n id = totalSupply;\n _mint(owner, 0);\n }\n\n function _tokenURI(uint256) internal view override returns (string memory) {\n return \"\";\n }\n}\n" + }, + "contracts/MinimalTimeLock.sol": { + "content": "//SPDX-License-Identifier: BSD-3-Clause\n\npragma solidity ^0.6.12;\npragma experimental ABIEncoderV2;\n\nimport \"@boringcrypto/boring-solidity/contracts/BoringOwnable.sol\";\n\n// Modified from https://etherscan.io/address/0x6d903f6003cca6255d85cca4d3b5e5146dc33925#code and https://github.com/boringcrypto/dictator-dao/blob/main/contracts/DictatorDAO.sol#L225\ncontract MinimalTimeLock is BoringOwnable { \n event QueueTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data, uint256 eta);\n event CancelTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data);\n event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint256 value, bytes data);\n\n uint256 public constant GRACE_PERIOD = 14 days;\n uint256 public constant DELAY = 2 days;\n mapping(bytes32 => uint256) public queuedTransactions;\n\n function queueTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner returns (bytes32) {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n uint256 eta = block.timestamp + DELAY;\n queuedTransactions[txHash] = eta;\n\n emit QueueTransaction(txHash, target, value, data, eta);\n return txHash;\n }\n\n function cancelTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n queuedTransactions[txHash] = 0;\n\n emit CancelTransaction(txHash, target, value, data);\n }\n\n function executeTransaction(\n address target,\n uint256 value,\n bytes memory data\n ) public onlyOwner payable returns (bytes memory) {\n\n bytes32 txHash = keccak256(abi.encode(target, value, data));\n uint256 eta = queuedTransactions[txHash];\n require(block.timestamp >= eta, \"Too early\");\n require(block.timestamp <= eta + GRACE_PERIOD, \"Tx stale\");\n\n queuedTransactions[txHash] = 0;\n\n // solium-disable-next-line security/no-call-value\n (bool success, bytes memory returnData) = target.call{value: value}(data);\n require(success, \"Tx reverted :(\");\n\n emit ExecuteTransaction(txHash, target, value, data);\n\n return returnData;\n }\n}" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": [ + "ast" + ] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} \ No newline at end of file From 3161f3b54916a41096efaec0c2cf9454c75f8b9f Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sat, 23 Apr 2022 07:31:01 +0200 Subject: [PATCH 069/107] (Whitelist master contract in Ropsten mock deployment) --- deploy/MockNFTPairEcoSystem.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/MockNFTPairEcoSystem.ts b/deploy/MockNFTPairEcoSystem.ts index c37c841e..7fcc26a6 100644 --- a/deploy/MockNFTPairEcoSystem.ts +++ b/deploy/MockNFTPairEcoSystem.ts @@ -67,6 +67,8 @@ const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnviro deterministicDeployment: false, }); + await bentoBox.whitelistMasterContract(nftPairMock.address, true); + // Pairs - deployed by BentoBox: const bentoDeploy = async (name, masterAddress, initData) => { try { From dbacda015b2b2c20f82848ae5d7bdc34e916892c Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:30:58 -0400 Subject: [PATCH 070/107] Added Oracle --- contracts/NFTPair2.sol | 744 ++++++++++++++++++++++++++++ contracts/interfaces/INFTOracle.sol | 37 ++ 2 files changed, 781 insertions(+) create mode 100644 contracts/NFTPair2.sol create mode 100644 contracts/interfaces/INFTOracle.sol diff --git a/contracts/NFTPair2.sol b/contracts/NFTPair2.sol new file mode 100644 index 00000000..aa735756 --- /dev/null +++ b/contracts/NFTPair2.sol @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: UNLICENSED + +// Private Pool (NFT collateral) + +// ( ( ( +// )\ ) ( )\ )\ ) ( +// (((_) ( /( ))\ ((_)(()/( )( ( ( +// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ ) +// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/( +// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \)) +// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_| + +// Copyright (c) 2021 BoringCrypto - All rights reserved +// Twitter: @Boring_Crypto + +// Special thanks to: +// @0xKeno - for all his invaluable contributions +// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "@boringcrypto/boring-solidity/contracts/Domain.sol"; +import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./interfaces/IERC721.sol"; +import "./interfaces/INFTOracle.sol"; + +struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 ltv; // + uint16 annualInterestBPS; // Variable cost of taking out the loan + INFTOracle oracle; // oracle used +} + +struct SignatureParams { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + +interface ILendingClub { + // Per token settings. + function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); + + function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); +} + +interface INFTPair { + function collateral() external view returns (IERC721); + + function asset() external view returns (IERC20); + + function masterContract() external view returns (address); + + function bentoBox() external view returns (IBentoBoxV1); + + function removeCollateral(uint256 tokenId, address to) external; +} + +/// @title NFTPair2 +/// @dev This contract allows contract calls to any contract (except BentoBox) +/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. +contract NFTPair2 is BoringOwnable, Domain, IMasterContract { + using BoringMath for uint256; + using BoringMath128 for uint128; + using RebaseLibrary for Rebase; + using BoringERC20 for IERC20; + + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + // This automatically clears the associated loan, if any + event LogRemoveCollateral(uint256 indexed tokenId, address recipient); + // Details are in the loan request + event LogLend(address indexed lender, uint256 indexed tokenId); + event LogRepay(address indexed from, uint256 indexed tokenId); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees(address indexed feeTo, uint256 feeShare); + + // Immutables (for MasterContract and all clones) + IBentoBoxV1 public immutable bentoBox; + NFTPair public immutable masterContract; + + // MasterContract variables + address public feeTo; + + // Per clone variables + // Clone init settings + IERC721 public collateral; + IERC20 public asset; + + // A note on terminology: + // "Shares" are BentoBox shares. + + // Track assets we own. Used to allow skimming the excesss. + uint256 public feesEarnedShare; + + // Per token settings. + mapping(uint256 => TokenLoanParams) public tokenLoanParams; + + uint8 private constant LOAN_INITIAL = 0; + uint8 private constant LOAN_REQUESTED = 1; + uint8 private constant LOAN_OUTSTANDING = 2; + struct TokenLoan { + address borrower; + address lender; + uint64 startTime; + uint8 status; + } + mapping(uint256 => TokenLoan) public tokenLoan; + + // Do not go over 100% on either of these.. + uint256 private constant PROTOCOL_FEE_BPS = 1000; + uint256 private constant OPEN_FEE_BPS = 100; + uint256 private constant BPS = 10_000; + uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; + + // Highest order term in the Maclaurin series for exp used by + // `calculateIntest`. + // Intuitive interpretation: interest continuously accrues on the principal. + // That interest, in turn, earns "second-order" interest-on-interest, which + // itself earns "third-order" interest, etc. This constant determines how + // far we take this until we stop counting. + // + // The error, in terms of the interest rate, is at least + // + // ----- n ----- Infinity + // \ x^k \ x^k + // e^x - ) --- , which is ) --- , + // / k! / k! + // ----- k = 1 k ----- k = n + 1 + // + // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of + // interest that is owed at rate r over time t. It makes no difference if + // this is, say, 5%/year for 10 years, or 50% in one year; the calculation + // is the same. Why "at least"? There are also rounding errors. See + // `calculateInterest` for more detail. + // The factorial in the denominator "wins"; for all reasonable (and quite + // a few unreasonable) interest rates, the lower-order terms contribute the + // most to the total. The following table lists some of the calculated + // approximations for different values of n, along with the "true" result: + // + // Total: 10% 20% 50% 100% 200% 500% 1000% + // ----------------------------------------------------------------------- + // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0% + // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0% + // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7% + // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3% + // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7% + // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6% + // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3% + // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1% + // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3% + // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5% + // + // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6% + // + // For instance, calculating the compounding effects of 200% in "total" + // interest to the sixth order results in 635.6%, whereas the true result + // is 638.9%. + // At 500% that difference is a little more dramatic, but it is still in + // the same ballpark -- and of little practical consequence unless the + // collateral can be expected to go up more than 112 times in value. + // Still, for volatile tokens, or an asset that is somehow known to be very + // inflationary, use a different number. + // Zero (no interest at all) is ignored and treated as one (linear only). + uint8 private constant COMPOUND_INTEREST_TERMS = 6; + + // For signed lend / borrow requests: + mapping(address => uint256) public nonces; + + /// @notice The constructor is only used for the initial master contract. + /// @notice Subsequent clones are initialised via `init`. + constructor(IBentoBoxV1 bentoBox_) public { + bentoBox = bentoBox_; + masterContract = this; + } + + /// @notice De facto constructor for clone contracts + function init(bytes calldata data) public payable override { + require(address(collateral) == address(0), "NFTPair: already initialized"); + (collateral, asset) = abi.decode(data, (IERC721, IERC20)); + require(address(collateral) != address(0), "NFTPair: bad pair"); + } + + function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { + TokenLoan memory loan = tokenLoan[tokenId]; + if (loan.status == LOAN_OUTSTANDING) { + // The lender can change terms so long as the changes are strictly + // the same or better for the borrower: + require(msg.sender == loan.lender, "NFTPair: not the lender"); + TokenLoanParams memory cur = tokenLoanParams[tokenId]; + require( + params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS, + "NFTPair: worse params" + ); + } else if (loan.status == LOAN_REQUESTED) { + // The borrower has already deposited the collateral and can + // change whatever they like + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else { + // The loan has not been taken out yet; the borrower needs to + // provide collateral. + revert("NFTPair: no collateral"); + } + tokenLoanParams[tokenId] = params; + emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); + } + + function _requestLoan( + address collateralProvider, + uint256 tokenId, + TokenLoanParams memory params, + address to, + bool skim + ) private { + // Edge case: valuation can be zero. That effectively gifts the NFT and + // is therefore a bad idea, but does not break the contract. + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + if (skim) { + require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); + } else { + collateral.transferFrom(collateralProvider, address(this), tokenId); + } + TokenLoan memory loan; + loan.borrower = to; + loan.status = LOAN_REQUESTED; + tokenLoan[tokenId] = loan; + tokenLoanParams[tokenId] = params; + + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); + } + + /// @notice Deposit an NFT as collateral and request a loan against it + /// @param tokenId ID of the NFT + /// @param to Address to receive the loan, or option to withdraw collateral + /// @param params Loan conditions on offer + /// @param skim True if the token has already been transfered + function requestLoan( + uint256 tokenId, + TokenLoanParams memory params, + address to, + bool skim + ) public { + _requestLoan(msg.sender, tokenId, params, to, skim); + } + + /// @notice Removes `tokenId` as collateral and transfers it to `to`. + /// @notice This destroys the loan. + /// @param tokenId The token + /// @param to The receiver of the token. + function removeCollateral(uint256 tokenId, address to) public { + TokenLoan memory loan = tokenLoan[tokenId]; + if (loan.status == LOAN_REQUESTED) { + // We are withdrawing collateral that is not in use: + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else if (loan.status == LOAN_OUTSTANDING) { + // We are seizing collateral as the lender. The loan has to be + // expired and not paid off: + require(msg.sender == loan.lender, "NFTPair: not the lender"); + + if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe: if this overflows, then all loans have expired anyway + uint256 interest = calculateInterest(loanParams.valuation, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 amount = loanParams.valuation + interest; + (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); + require (rate.mul(loanParams.ltv) / BPS < amount, "NFT is still valued"); + } + } + // If there somehow is collateral but no accompanying loan, then anyone + // can claim it by first requesting a loan with `skim` set to true, and + // then withdrawing. So we might as well allow it here.. + delete tokenLoan[tokenId]; + collateral.transferFrom(address(this), to, tokenId); + emit LogRemoveCollateral(tokenId, to); + } + + // Assumes the lender has agreed to the loan. + function _lend( + address lender, + uint256 tokenId, + TokenLoanParams memory accepted, + bool skim + ) internal { + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); + TokenLoanParams memory params = tokenLoanParams[tokenId]; + + // Valuation has to be an exact match, everything else must be at least + // as good for the lender as `accepted`. + require( + params.valuation == accepted.valuation && + params.duration <= accepted.duration && + params.annualInterestBPS >= accepted.annualInterestBPS, + "NFTPair: bad params" + ); + + if (params.oracle != INFTOracle(0)) { + (, uint256 rate) = params.oracle.get(address(this), tokenId); + require(rate.mul(uint256(params.ltv)) / BPS >= params.valuation, "Oracle: price too low."); + } + + uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); + // No overflow: at most 128 + 16 bits (fits in BentoBox) + uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; + uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; + + if (skim) { + require( + bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), + "NFTPair: skim too much" + ); + } else { + bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); + } + // No underflow: follows from OPEN_FEE_BPS <= BPS + uint256 borrowerShare = totalShare - openFeeShare; + bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); + // No overflow: addends (and result) must fit in BentoBox + feesEarnedShare += protocolFeeShare; + + loan.lender = lender; + loan.status = LOAN_OUTSTANDING; + loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. + tokenLoan[tokenId] = loan; + + emit LogLend(lender, tokenId); + } + + /// @notice Lends with the parameters specified by the borrower. + /// @param tokenId ID of the token that will function as collateral + /// @param accepted Loan parameters as the lender saw them, for security + /// @param skim True if the funds have been transfered to the contract + function lend( + uint256 tokenId, + TokenLoanParams memory accepted, + bool skim + ) public { + _lend(msg.sender, tokenId, accepted, skim); + } + + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + // NOTE on signature hashes: the domain separator only guarantees that the + // chain ID and master contract are a match, so we explicitly include the + // clone address (and the asset/collateral addresses): + + // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; + + // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336; + + /// @notice Request and immediately borrow from a pre-committed lender + + /// @notice Caller provides collateral; loan can go to a different address. + /// @param tokenId ID of the token that will function as collateral + /// @param lender Lender, whose BentoBox balance the funds will come from + /// @param recipient Address to receive the loan. + /// @param params Loan parameters requested, and signed by the lender + /// @param skimCollateral True if the collateral has already been transfered + /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + function requestAndBorrow( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + bool anyTokenId, + SignatureParams memory signature + ) public { + if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { + require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); + } else { + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[lender]++; + bytes32 dataHash = keccak256( + abi.encode( + LEND_SIGNATURE_HASH, + address(this), + anyTokenId ? 0 : tokenId, + anyTokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + params.ltv, + params.oracle, + nonce, + signature.deadline + ) + ); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == lender, "NFTPair: signature invalid"); + } + _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); + _lend(lender, tokenId, params, false); + } + + /// @notice Take collateral from a pre-commited borrower and lend against it + /// @notice Collateral must come from the borrower, not a third party. + /// @param tokenId ID of the token that will function as collateral + /// @param borrower Address that provides collateral and receives the loan + /// @param params Loan terms offered, and signed by the borrower + /// @param skimFunds True if the funds have been transfered to the contract + function takeCollateralAndLend( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + SignatureParams memory signature + ) public { + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[borrower]++; + bytes32 dataHash = keccak256( + abi.encode( + BORROW_SIGNATURE_HASH, + address(this), + tokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + params.ltv, + params.oracle, + nonce, + signature.deadline + ) + ); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == borrower, "NFTPair: signature invalid"); + _requestLoan(borrower, tokenId, params, borrower, false); + _lend(msg.sender, tokenId, params, skimFunds); + } + + /// Approximates continuous compounding. Uses Horner's method to evaluate + /// the truncated Maclaurin series for exp - 1, accumulating rounding + /// errors along the way. The following is always guaranteed: + /// + /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), + /// + /// where time = t/YEAR, up to at most the rounding error obtained in + /// calculating linear interest. + /// + /// If the theoretical result that we are approximating (the rightmost part + /// of the above inquality) fits in 128 bits, then the function is + /// guaranteed not to revert (unless n > 250, which is way too high). + /// If even the linear interest (leftmost part of the inequality) does not + /// the function will revert. + /// Otherwise, the function may revert, return a reasonable result, or + /// return a very inaccurate result. Even then the above inequality is + /// respected. + function calculateInterest( + uint256 principal, + uint64 t, + uint16 aprBPS + ) public pure returns (uint256 interest) { + // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS) + // + // We calculate + // + // ----- n ----- n + // \ principal * (t * aprBPS)^k \ + // ) -------------------------- =: ) term_k + // / k! * YEAR_BPS^k / + // ----- k = 1 ----- k = 1 + // + // which approaches, but never exceeds the "theoretical" result, + // + // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 + // + // as n goes to infinity. We use the fact that + // + // principal * (t * aprBPS)^(k-1) * (t * aprBPS) + // term_k = --------------------------------------------- + // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS + // + // t * aprBPS + // = term_{k-1} * ------------ (*) + // k * YEAR_BPS + // + // to calculate the terms one by one. The principal affords us the + // precision to carry out the division without resorting to fixed-point + // math. Any rounding error is downward, which we consider acceptable. + // + // Since all numbers involved are positive, each term is certainly + // bounded above by M. From (*) we see that any intermediate results + // are at most + // + // denom_k := k * YEAR_BPS. + // + // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits, + // which proves that all calculations will certainly not overflow if M + // fits in 128 bits. + // + // If M does not fit, then the intermediate results for some term may + // eventually overflow, but this cannot happen at the first term, and + // neither can the total overflow because it uses checked math. + // + // This constitutes a guarantee of specified behavior when M >= 2^128. + uint256 x = uint256(t) * aprBPS; + uint256 term_k = (principal * x) / YEAR_BPS; + uint256 denom_k = YEAR_BPS; + + interest = term_k; + for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { + denom_k += YEAR_BPS; + term_k = (term_k * x) / denom_k; + interest = interest.add(term_k); // <- Only overflow check we need + } + + if (interest >= 2**128) { + revert(); + } + } + + function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + require( + // Addition is safe: both summands are smaller than 256 bits + uint256(loan.startTime) + loanParams.duration > block.timestamp, + "NFTPair: loan expired" + ); + + uint128 principal = loanParams.valuation; + + // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe: if this overflows, then all loans have expired anyway + uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; + amount = principal + interest; + + uint256 totalShare = bentoBox.toShare(asset, amount, false); + uint256 feeShare = bentoBox.toShare(asset, fee, false); + + address from; + if (skim) { + require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + from = address(this); + // No overflow: result fits in BentoBox + } else { + bentoBox.transfer(asset, msg.sender, address(this), feeShare); + from = msg.sender; + } + // No underflow: PROTOCOL_FEE_BPS < BPS by construction. + feesEarnedShare += feeShare; + delete tokenLoan[tokenId]; + + bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); + collateral.transferFrom(address(this), loan.borrower, tokenId); + + emit LogRepay(from, tokenId); + } + + uint8 internal constant ACTION_REPAY = 2; + uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; + + uint8 internal constant ACTION_REQUEST_LOAN = 12; + uint8 internal constant ACTION_LEND = 13; + + // Function on BentoBox + uint8 internal constant ACTION_BENTO_DEPOSIT = 20; + uint8 internal constant ACTION_BENTO_WITHDRAW = 21; + uint8 internal constant ACTION_BENTO_TRANSFER = 22; + uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; + uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; + + // Any external call (except to BentoBox) + uint8 internal constant ACTION_CALL = 30; + + // Signed requests + uint8 internal constant ACTION_REQUEST_AND_BORROW = 40; + uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41; + + int256 internal constant USE_VALUE1 = -1; + int256 internal constant USE_VALUE2 = -2; + + /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. + function _num( + int256 inNum, + uint256 value1, + uint256 value2 + ) internal pure returns (uint256 outNum) { + outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); + } + + /// @dev Helper function for depositing into `bentoBox`. + function _bentoDeposit( + bytes memory data, + uint256 value, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors + share = int256(_num(share, value1, value2)); + return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); + } + + /// @dev Helper function to withdraw from the `bentoBox`. + function _bentoWithdraw( + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); + } + + /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. + /// Calls to `bentoBox` or `collateral` are not allowed for security reasons. + /// This also means that calls made from this contract shall *not* be trusted. + function _call( + uint256 value, + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (bytes memory, uint8) { + (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( + data, + (address, bytes, bool, bool, uint8) + ); + + if (useValue1 && !useValue2) { + callData = abi.encodePacked(callData, value1); + } else if (!useValue1 && useValue2) { + callData = abi.encodePacked(callData, value2); + } else if (useValue1 && useValue2) { + callData = abi.encodePacked(callData, value1, value2); + } + + require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), "NFTPair: can't call"); + + (bool success, bytes memory returnData) = callee.call{value: value}(callData); + require(success, "NFTPair: call failed"); + return (returnData, returnValues); + } + + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + for (uint256 i = 0; i < actions.length; i++) { + uint8 action = actions[i]; + if (action == ACTION_REPAY) { + (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); + repay(tokenId, skim); + } else if (action == ACTION_REMOVE_COLLATERAL) { + (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); + removeCollateral(tokenId, to); + } else if (action == ACTION_REQUEST_LOAN) { + (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode( + datas[i], + (uint256, TokenLoanParams, address, bool) + ); + requestLoan(tokenId, params, to, skim); + } else if (action == ACTION_LEND) { + (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool)); + lend(tokenId, params, skim); + } else if (action == ACTION_BENTO_SETAPPROVAL) { + (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( + datas[i], + (address, address, bool, uint8, bytes32, bytes32) + ); + bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); + } else if (action == ACTION_BENTO_WITHDRAW) { + (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + } else if (action == ACTION_BENTO_TRANSFER) { + (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); + bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); + } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { + (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); + bentoBox.transferMultiple(token, msg.sender, tos, shares); + } else if (action == ACTION_CALL) { + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); + + if (returnValues == 1) { + (value1) = abi.decode(returnData, (uint256)); + } else if (returnValues == 2) { + (value1, value2) = abi.decode(returnData, (uint256, uint256)); + } + } else if (action == ACTION_REQUEST_AND_BORROW) { + ( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + bool anyTokenId, + SignatureParams memory signature + ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams)); + requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); + } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { + ( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + SignatureParams memory signature + ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); + takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); + } + } + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + address to = masterContract.feeTo(); + + uint256 _share = feesEarnedShare; + if (_share > 0) { + bentoBox.transfer(asset, address(this), to, _share); + feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, _share); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } +} diff --git a/contracts/interfaces/INFTOracle.sol b/contracts/interfaces/INFTOracle.sol new file mode 100644 index 00000000..148f55cb --- /dev/null +++ b/contracts/interfaces/INFTOracle.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity >= 0.6.12; + +interface INFTOracle { + /// @notice Get the latest exchange rate. + /// @param pair address of the NFTPair calling the oracle + /// @param tokenId tokenId of the NFT in question + /// @return success if no valid (recent) rate is available, return false else true. + /// @return rate The rate of the requested asset / pair / pool. + function get(address pair, uint256 tokenId) external returns (bool success, uint256 rate); + + /// @notice Check the last exchange rate without any state changes. + /// @param pair address of the NFTPair calling the oracle + /// @param tokenId tokenId of the NFT in question + /// @return success if no valid (recent) rate is available, return false else true. + /// @return rate The rate of the requested asset / pair / pool. + function peek(address pair, uint256 tokenId) external view returns (bool success, uint256 rate); + + /// @notice Check the current spot exchange rate without any state changes. For oracles like TWAP this will be different from peek(). + /// @param pair address of the NFTPair calling the oracle + /// @param tokenId tokenId of the NFT in question + /// (string memory collateralSymbol, string memory assetSymbol, uint256 division) = abi.decode(data, (string, string, uint256)); + /// @return rate The rate of the requested asset / pair / pool. + function peekSpot(address pair, uint256 tokenId) external view returns (uint256 rate); + + /// @notice Returns a human readable (short) name about this oracle. + /// @param pair address of the NFTPair calling the oracle + /// @param tokenId tokenId of the NFT in question + /// @return (string) A human readable symbol name about this oracle. + function symbol(address pair, uint256 tokenId) external view returns (string memory); + + /// @notice Returns a human readable name about this oracle. + /// @param pair address of the NFTPair calling the oracle + /// @param tokenId tokenId of the NFT in question + /// @return (string) A human readable name about this oracle. + function name(address pair, uint256 tokenId) external view returns (string memory); +} From 6bd5af7c21f886b7699f8df0579174e2dd3828a0 Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:33:57 -0400 Subject: [PATCH 071/107] renamed contract --- contracts/NFTPairWithOracle.sol | 744 ++++++++++++++++++++++++++++++++ 1 file changed, 744 insertions(+) create mode 100644 contracts/NFTPairWithOracle.sol diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol new file mode 100644 index 00000000..1bb89e94 --- /dev/null +++ b/contracts/NFTPairWithOracle.sol @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: UNLICENSED + +// Private Pool (NFT collateral) + +// ( ( ( +// )\ ) ( )\ )\ ) ( +// (((_) ( /( ))\ ((_)(()/( )( ( ( +// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ ) +// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/( +// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \)) +// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_| + +// Copyright (c) 2021 BoringCrypto - All rights reserved +// Twitter: @Boring_Crypto + +// Special thanks to: +// @0xKeno - for all his invaluable contributions +// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "@boringcrypto/boring-solidity/contracts/Domain.sol"; +import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./interfaces/IERC721.sol"; +import "./interfaces/INFTOracle.sol"; + +struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 ltv; // + uint16 annualInterestBPS; // Variable cost of taking out the loan + INFTOracle oracle; // oracle used +} + +struct SignatureParams { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + +interface ILendingClub { + // Per token settings. + function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); + + function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); +} + +interface INFTPair { + function collateral() external view returns (IERC721); + + function asset() external view returns (IERC20); + + function masterContract() external view returns (address); + + function bentoBox() external view returns (IBentoBoxV1); + + function removeCollateral(uint256 tokenId, address to) external; +} + +/// @title NFTPairWithOracle +/// @dev This contract allows contract calls to any contract (except BentoBox) +/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. +contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { + using BoringMath for uint256; + using BoringMath128 for uint128; + using RebaseLibrary for Rebase; + using BoringERC20 for IERC20; + + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + // This automatically clears the associated loan, if any + event LogRemoveCollateral(uint256 indexed tokenId, address recipient); + // Details are in the loan request + event LogLend(address indexed lender, uint256 indexed tokenId); + event LogRepay(address indexed from, uint256 indexed tokenId); + event LogFeeTo(address indexed newFeeTo); + event LogWithdrawFees(address indexed feeTo, uint256 feeShare); + + // Immutables (for MasterContract and all clones) + IBentoBoxV1 public immutable bentoBox; + NFTPair public immutable masterContract; + + // MasterContract variables + address public feeTo; + + // Per clone variables + // Clone init settings + IERC721 public collateral; + IERC20 public asset; + + // A note on terminology: + // "Shares" are BentoBox shares. + + // Track assets we own. Used to allow skimming the excesss. + uint256 public feesEarnedShare; + + // Per token settings. + mapping(uint256 => TokenLoanParams) public tokenLoanParams; + + uint8 private constant LOAN_INITIAL = 0; + uint8 private constant LOAN_REQUESTED = 1; + uint8 private constant LOAN_OUTSTANDING = 2; + struct TokenLoan { + address borrower; + address lender; + uint64 startTime; + uint8 status; + } + mapping(uint256 => TokenLoan) public tokenLoan; + + // Do not go over 100% on either of these.. + uint256 private constant PROTOCOL_FEE_BPS = 1000; + uint256 private constant OPEN_FEE_BPS = 100; + uint256 private constant BPS = 10_000; + uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; + + // Highest order term in the Maclaurin series for exp used by + // `calculateIntest`. + // Intuitive interpretation: interest continuously accrues on the principal. + // That interest, in turn, earns "second-order" interest-on-interest, which + // itself earns "third-order" interest, etc. This constant determines how + // far we take this until we stop counting. + // + // The error, in terms of the interest rate, is at least + // + // ----- n ----- Infinity + // \ x^k \ x^k + // e^x - ) --- , which is ) --- , + // / k! / k! + // ----- k = 1 k ----- k = n + 1 + // + // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of + // interest that is owed at rate r over time t. It makes no difference if + // this is, say, 5%/year for 10 years, or 50% in one year; the calculation + // is the same. Why "at least"? There are also rounding errors. See + // `calculateInterest` for more detail. + // The factorial in the denominator "wins"; for all reasonable (and quite + // a few unreasonable) interest rates, the lower-order terms contribute the + // most to the total. The following table lists some of the calculated + // approximations for different values of n, along with the "true" result: + // + // Total: 10% 20% 50% 100% 200% 500% 1000% + // ----------------------------------------------------------------------- + // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0% + // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0% + // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7% + // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3% + // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7% + // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6% + // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3% + // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1% + // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3% + // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5% + // + // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6% + // + // For instance, calculating the compounding effects of 200% in "total" + // interest to the sixth order results in 635.6%, whereas the true result + // is 638.9%. + // At 500% that difference is a little more dramatic, but it is still in + // the same ballpark -- and of little practical consequence unless the + // collateral can be expected to go up more than 112 times in value. + // Still, for volatile tokens, or an asset that is somehow known to be very + // inflationary, use a different number. + // Zero (no interest at all) is ignored and treated as one (linear only). + uint8 private constant COMPOUND_INTEREST_TERMS = 6; + + // For signed lend / borrow requests: + mapping(address => uint256) public nonces; + + /// @notice The constructor is only used for the initial master contract. + /// @notice Subsequent clones are initialised via `init`. + constructor(IBentoBoxV1 bentoBox_) public { + bentoBox = bentoBox_; + masterContract = this; + } + + /// @notice De facto constructor for clone contracts + function init(bytes calldata data) public payable override { + require(address(collateral) == address(0), "NFTPair: already initialized"); + (collateral, asset) = abi.decode(data, (IERC721, IERC20)); + require(address(collateral) != address(0), "NFTPair: bad pair"); + } + + function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { + TokenLoan memory loan = tokenLoan[tokenId]; + if (loan.status == LOAN_OUTSTANDING) { + // The lender can change terms so long as the changes are strictly + // the same or better for the borrower: + require(msg.sender == loan.lender, "NFTPair: not the lender"); + TokenLoanParams memory cur = tokenLoanParams[tokenId]; + require( + params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS, + "NFTPair: worse params" + ); + } else if (loan.status == LOAN_REQUESTED) { + // The borrower has already deposited the collateral and can + // change whatever they like + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else { + // The loan has not been taken out yet; the borrower needs to + // provide collateral. + revert("NFTPair: no collateral"); + } + tokenLoanParams[tokenId] = params; + emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); + } + + function _requestLoan( + address collateralProvider, + uint256 tokenId, + TokenLoanParams memory params, + address to, + bool skim + ) private { + // Edge case: valuation can be zero. That effectively gifts the NFT and + // is therefore a bad idea, but does not break the contract. + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + if (skim) { + require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); + } else { + collateral.transferFrom(collateralProvider, address(this), tokenId); + } + TokenLoan memory loan; + loan.borrower = to; + loan.status = LOAN_REQUESTED; + tokenLoan[tokenId] = loan; + tokenLoanParams[tokenId] = params; + + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); + } + + /// @notice Deposit an NFT as collateral and request a loan against it + /// @param tokenId ID of the NFT + /// @param to Address to receive the loan, or option to withdraw collateral + /// @param params Loan conditions on offer + /// @param skim True if the token has already been transfered + function requestLoan( + uint256 tokenId, + TokenLoanParams memory params, + address to, + bool skim + ) public { + _requestLoan(msg.sender, tokenId, params, to, skim); + } + + /// @notice Removes `tokenId` as collateral and transfers it to `to`. + /// @notice This destroys the loan. + /// @param tokenId The token + /// @param to The receiver of the token. + function removeCollateral(uint256 tokenId, address to) public { + TokenLoan memory loan = tokenLoan[tokenId]; + if (loan.status == LOAN_REQUESTED) { + // We are withdrawing collateral that is not in use: + require(msg.sender == loan.borrower, "NFTPair: not the borrower"); + } else if (loan.status == LOAN_OUTSTANDING) { + // We are seizing collateral as the lender. The loan has to be + // expired and not paid off: + require(msg.sender == loan.lender, "NFTPair: not the lender"); + + if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe: if this overflows, then all loans have expired anyway + uint256 interest = calculateInterest(loanParams.valuation, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 amount = loanParams.valuation + interest; + (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); + require (rate.mul(loanParams.ltv) / BPS < amount, "NFT is still valued"); + } + } + // If there somehow is collateral but no accompanying loan, then anyone + // can claim it by first requesting a loan with `skim` set to true, and + // then withdrawing. So we might as well allow it here.. + delete tokenLoan[tokenId]; + collateral.transferFrom(address(this), to, tokenId); + emit LogRemoveCollateral(tokenId, to); + } + + // Assumes the lender has agreed to the loan. + function _lend( + address lender, + uint256 tokenId, + TokenLoanParams memory accepted, + bool skim + ) internal { + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); + TokenLoanParams memory params = tokenLoanParams[tokenId]; + + // Valuation has to be an exact match, everything else must be at least + // as good for the lender as `accepted`. + require( + params.valuation == accepted.valuation && + params.duration <= accepted.duration && + params.annualInterestBPS >= accepted.annualInterestBPS, + "NFTPair: bad params" + ); + + if (params.oracle != INFTOracle(0)) { + (, uint256 rate) = params.oracle.get(address(this), tokenId); + require(rate.mul(uint256(params.ltv)) / BPS >= params.valuation, "Oracle: price too low."); + } + + uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); + // No overflow: at most 128 + 16 bits (fits in BentoBox) + uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; + uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; + + if (skim) { + require( + bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), + "NFTPair: skim too much" + ); + } else { + bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); + } + // No underflow: follows from OPEN_FEE_BPS <= BPS + uint256 borrowerShare = totalShare - openFeeShare; + bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); + // No overflow: addends (and result) must fit in BentoBox + feesEarnedShare += protocolFeeShare; + + loan.lender = lender; + loan.status = LOAN_OUTSTANDING; + loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. + tokenLoan[tokenId] = loan; + + emit LogLend(lender, tokenId); + } + + /// @notice Lends with the parameters specified by the borrower. + /// @param tokenId ID of the token that will function as collateral + /// @param accepted Loan parameters as the lender saw them, for security + /// @param skim True if the funds have been transfered to the contract + function lend( + uint256 tokenId, + TokenLoanParams memory accepted, + bool skim + ) public { + _lend(msg.sender, tokenId, accepted, skim); + } + + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + // NOTE on signature hashes: the domain separator only guarantees that the + // chain ID and master contract are a match, so we explicitly include the + // clone address (and the asset/collateral addresses): + + // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; + + // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336; + + /// @notice Request and immediately borrow from a pre-committed lender + + /// @notice Caller provides collateral; loan can go to a different address. + /// @param tokenId ID of the token that will function as collateral + /// @param lender Lender, whose BentoBox balance the funds will come from + /// @param recipient Address to receive the loan. + /// @param params Loan parameters requested, and signed by the lender + /// @param skimCollateral True if the collateral has already been transfered + /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + function requestAndBorrow( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + bool anyTokenId, + SignatureParams memory signature + ) public { + if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { + require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); + } else { + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[lender]++; + bytes32 dataHash = keccak256( + abi.encode( + LEND_SIGNATURE_HASH, + address(this), + anyTokenId ? 0 : tokenId, + anyTokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + params.ltv, + params.oracle, + nonce, + signature.deadline + ) + ); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == lender, "NFTPair: signature invalid"); + } + _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); + _lend(lender, tokenId, params, false); + } + + /// @notice Take collateral from a pre-commited borrower and lend against it + /// @notice Collateral must come from the borrower, not a third party. + /// @param tokenId ID of the token that will function as collateral + /// @param borrower Address that provides collateral and receives the loan + /// @param params Loan terms offered, and signed by the borrower + /// @param skimFunds True if the funds have been transfered to the contract + function takeCollateralAndLend( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + SignatureParams memory signature + ) public { + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); + uint256 nonce = nonces[borrower]++; + bytes32 dataHash = keccak256( + abi.encode( + BORROW_SIGNATURE_HASH, + address(this), + tokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + params.ltv, + params.oracle, + nonce, + signature.deadline + ) + ); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == borrower, "NFTPair: signature invalid"); + _requestLoan(borrower, tokenId, params, borrower, false); + _lend(msg.sender, tokenId, params, skimFunds); + } + + /// Approximates continuous compounding. Uses Horner's method to evaluate + /// the truncated Maclaurin series for exp - 1, accumulating rounding + /// errors along the way. The following is always guaranteed: + /// + /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), + /// + /// where time = t/YEAR, up to at most the rounding error obtained in + /// calculating linear interest. + /// + /// If the theoretical result that we are approximating (the rightmost part + /// of the above inquality) fits in 128 bits, then the function is + /// guaranteed not to revert (unless n > 250, which is way too high). + /// If even the linear interest (leftmost part of the inequality) does not + /// the function will revert. + /// Otherwise, the function may revert, return a reasonable result, or + /// return a very inaccurate result. Even then the above inequality is + /// respected. + function calculateInterest( + uint256 principal, + uint64 t, + uint16 aprBPS + ) public pure returns (uint256 interest) { + // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS) + // + // We calculate + // + // ----- n ----- n + // \ principal * (t * aprBPS)^k \ + // ) -------------------------- =: ) term_k + // / k! * YEAR_BPS^k / + // ----- k = 1 ----- k = 1 + // + // which approaches, but never exceeds the "theoretical" result, + // + // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 + // + // as n goes to infinity. We use the fact that + // + // principal * (t * aprBPS)^(k-1) * (t * aprBPS) + // term_k = --------------------------------------------- + // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS + // + // t * aprBPS + // = term_{k-1} * ------------ (*) + // k * YEAR_BPS + // + // to calculate the terms one by one. The principal affords us the + // precision to carry out the division without resorting to fixed-point + // math. Any rounding error is downward, which we consider acceptable. + // + // Since all numbers involved are positive, each term is certainly + // bounded above by M. From (*) we see that any intermediate results + // are at most + // + // denom_k := k * YEAR_BPS. + // + // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits, + // which proves that all calculations will certainly not overflow if M + // fits in 128 bits. + // + // If M does not fit, then the intermediate results for some term may + // eventually overflow, but this cannot happen at the first term, and + // neither can the total overflow because it uses checked math. + // + // This constitutes a guarantee of specified behavior when M >= 2^128. + uint256 x = uint256(t) * aprBPS; + uint256 term_k = (principal * x) / YEAR_BPS; + uint256 denom_k = YEAR_BPS; + + interest = term_k; + for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { + denom_k += YEAR_BPS; + term_k = (term_k * x) / denom_k; + interest = interest.add(term_k); // <- Only overflow check we need + } + + if (interest >= 2**128) { + revert(); + } + } + + function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + require( + // Addition is safe: both summands are smaller than 256 bits + uint256(loan.startTime) + loanParams.duration > block.timestamp, + "NFTPair: loan expired" + ); + + uint128 principal = loanParams.valuation; + + // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe: if this overflows, then all loans have expired anyway + uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; + amount = principal + interest; + + uint256 totalShare = bentoBox.toShare(asset, amount, false); + uint256 feeShare = bentoBox.toShare(asset, fee, false); + + address from; + if (skim) { + require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + from = address(this); + // No overflow: result fits in BentoBox + } else { + bentoBox.transfer(asset, msg.sender, address(this), feeShare); + from = msg.sender; + } + // No underflow: PROTOCOL_FEE_BPS < BPS by construction. + feesEarnedShare += feeShare; + delete tokenLoan[tokenId]; + + bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); + collateral.transferFrom(address(this), loan.borrower, tokenId); + + emit LogRepay(from, tokenId); + } + + uint8 internal constant ACTION_REPAY = 2; + uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; + + uint8 internal constant ACTION_REQUEST_LOAN = 12; + uint8 internal constant ACTION_LEND = 13; + + // Function on BentoBox + uint8 internal constant ACTION_BENTO_DEPOSIT = 20; + uint8 internal constant ACTION_BENTO_WITHDRAW = 21; + uint8 internal constant ACTION_BENTO_TRANSFER = 22; + uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; + uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; + + // Any external call (except to BentoBox) + uint8 internal constant ACTION_CALL = 30; + + // Signed requests + uint8 internal constant ACTION_REQUEST_AND_BORROW = 40; + uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41; + + int256 internal constant USE_VALUE1 = -1; + int256 internal constant USE_VALUE2 = -2; + + /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. + function _num( + int256 inNum, + uint256 value1, + uint256 value2 + ) internal pure returns (uint256 outNum) { + outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); + } + + /// @dev Helper function for depositing into `bentoBox`. + function _bentoDeposit( + bytes memory data, + uint256 value, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors + share = int256(_num(share, value1, value2)); + return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); + } + + /// @dev Helper function to withdraw from the `bentoBox`. + function _bentoWithdraw( + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (uint256, uint256) { + (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); + return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); + } + + /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. + /// Calls to `bentoBox` or `collateral` are not allowed for security reasons. + /// This also means that calls made from this contract shall *not* be trusted. + function _call( + uint256 value, + bytes memory data, + uint256 value1, + uint256 value2 + ) internal returns (bytes memory, uint8) { + (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( + data, + (address, bytes, bool, bool, uint8) + ); + + if (useValue1 && !useValue2) { + callData = abi.encodePacked(callData, value1); + } else if (!useValue1 && useValue2) { + callData = abi.encodePacked(callData, value2); + } else if (useValue1 && useValue2) { + callData = abi.encodePacked(callData, value1, value2); + } + + require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), "NFTPair: can't call"); + + (bool success, bytes memory returnData) = callee.call{value: value}(callData); + require(success, "NFTPair: call failed"); + return (returnData, returnValues); + } + + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + for (uint256 i = 0; i < actions.length; i++) { + uint8 action = actions[i]; + if (action == ACTION_REPAY) { + (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); + repay(tokenId, skim); + } else if (action == ACTION_REMOVE_COLLATERAL) { + (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); + removeCollateral(tokenId, to); + } else if (action == ACTION_REQUEST_LOAN) { + (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode( + datas[i], + (uint256, TokenLoanParams, address, bool) + ); + requestLoan(tokenId, params, to, skim); + } else if (action == ACTION_LEND) { + (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool)); + lend(tokenId, params, skim); + } else if (action == ACTION_BENTO_SETAPPROVAL) { + (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( + datas[i], + (address, address, bool, uint8, bytes32, bytes32) + ); + bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); + } else if (action == ACTION_BENTO_DEPOSIT) { + (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); + } else if (action == ACTION_BENTO_WITHDRAW) { + (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + } else if (action == ACTION_BENTO_TRANSFER) { + (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); + bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); + } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { + (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); + bentoBox.transferMultiple(token, msg.sender, tos, shares); + } else if (action == ACTION_CALL) { + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); + + if (returnValues == 1) { + (value1) = abi.decode(returnData, (uint256)); + } else if (returnValues == 2) { + (value1, value2) = abi.decode(returnData, (uint256, uint256)); + } + } else if (action == ACTION_REQUEST_AND_BORROW) { + ( + uint256 tokenId, + address lender, + address recipient, + TokenLoanParams memory params, + bool skimCollateral, + bool anyTokenId, + SignatureParams memory signature + ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams)); + requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); + } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { + ( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + SignatureParams memory signature + ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); + takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); + } + } + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + address to = masterContract.feeTo(); + + uint256 _share = feesEarnedShare; + if (_share > 0) { + bentoBox.transfer(asset, address(this), to, _share); + feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, _share); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } +} From 750b1ae5e1f2225d4d4ae0c65456f208d85f715e Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:34:49 -0400 Subject: [PATCH 072/107] Removed renamed contract --- contracts/NFTPair2.sol | 744 ----------------------------------------- 1 file changed, 744 deletions(-) delete mode 100644 contracts/NFTPair2.sol diff --git a/contracts/NFTPair2.sol b/contracts/NFTPair2.sol deleted file mode 100644 index aa735756..00000000 --- a/contracts/NFTPair2.sol +++ /dev/null @@ -1,744 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED - -// Private Pool (NFT collateral) - -// ( ( ( -// )\ ) ( )\ )\ ) ( -// (((_) ( /( ))\ ((_)(()/( )( ( ( -// )\___ )(_)) /((_) _ ((_))(()\ )\ )\ ) -// ((/ __|((_)_ (_))( | | _| | ((_) ((_) _(_/( -// | (__ / _` || || || |/ _` | | '_|/ _ \| ' \)) -// \___|\__,_| \_,_||_|\__,_| |_| \___/|_||_| - -// Copyright (c) 2021 BoringCrypto - All rights reserved -// Twitter: @Boring_Crypto - -// Special thanks to: -// @0xKeno - for all his invaluable contributions -// @burger_crypto - for the idea of trying to let the LPs benefit from liquidations - -pragma solidity 0.6.12; -pragma experimental ABIEncoderV2; -import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; -import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; -import "@boringcrypto/boring-solidity/contracts/Domain.sol"; -import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; -import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; -import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; -import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; -import "./interfaces/IERC721.sol"; -import "./interfaces/INFTOracle.sol"; - -struct TokenLoanParams { - uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 duration; // Length of loan in seconds - uint16 ltv; // - uint16 annualInterestBPS; // Variable cost of taking out the loan - INFTOracle oracle; // oracle used -} - -struct SignatureParams { - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; -} - -interface ILendingClub { - // Per token settings. - function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); - - function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); -} - -interface INFTPair { - function collateral() external view returns (IERC721); - - function asset() external view returns (IERC20); - - function masterContract() external view returns (address); - - function bentoBox() external view returns (IBentoBoxV1); - - function removeCollateral(uint256 tokenId, address to) external; -} - -/// @title NFTPair2 -/// @dev This contract allows contract calls to any contract (except BentoBox) -/// from arbitrary callers thus, don't trust calls from this contract in any circumstances. -contract NFTPair2 is BoringOwnable, Domain, IMasterContract { - using BoringMath for uint256; - using BoringMath128 for uint128; - using RebaseLibrary for Rebase; - using BoringERC20 for IERC20; - - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); - // This automatically clears the associated loan, if any - event LogRemoveCollateral(uint256 indexed tokenId, address recipient); - // Details are in the loan request - event LogLend(address indexed lender, uint256 indexed tokenId); - event LogRepay(address indexed from, uint256 indexed tokenId); - event LogFeeTo(address indexed newFeeTo); - event LogWithdrawFees(address indexed feeTo, uint256 feeShare); - - // Immutables (for MasterContract and all clones) - IBentoBoxV1 public immutable bentoBox; - NFTPair public immutable masterContract; - - // MasterContract variables - address public feeTo; - - // Per clone variables - // Clone init settings - IERC721 public collateral; - IERC20 public asset; - - // A note on terminology: - // "Shares" are BentoBox shares. - - // Track assets we own. Used to allow skimming the excesss. - uint256 public feesEarnedShare; - - // Per token settings. - mapping(uint256 => TokenLoanParams) public tokenLoanParams; - - uint8 private constant LOAN_INITIAL = 0; - uint8 private constant LOAN_REQUESTED = 1; - uint8 private constant LOAN_OUTSTANDING = 2; - struct TokenLoan { - address borrower; - address lender; - uint64 startTime; - uint8 status; - } - mapping(uint256 => TokenLoan) public tokenLoan; - - // Do not go over 100% on either of these.. - uint256 private constant PROTOCOL_FEE_BPS = 1000; - uint256 private constant OPEN_FEE_BPS = 100; - uint256 private constant BPS = 10_000; - uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; - - // Highest order term in the Maclaurin series for exp used by - // `calculateIntest`. - // Intuitive interpretation: interest continuously accrues on the principal. - // That interest, in turn, earns "second-order" interest-on-interest, which - // itself earns "third-order" interest, etc. This constant determines how - // far we take this until we stop counting. - // - // The error, in terms of the interest rate, is at least - // - // ----- n ----- Infinity - // \ x^k \ x^k - // e^x - ) --- , which is ) --- , - // / k! / k! - // ----- k = 1 k ----- k = n + 1 - // - // where n = COMPOUND_INTEREST_TERMS, and x = rt is the total amount of - // interest that is owed at rate r over time t. It makes no difference if - // this is, say, 5%/year for 10 years, or 50% in one year; the calculation - // is the same. Why "at least"? There are also rounding errors. See - // `calculateInterest` for more detail. - // The factorial in the denominator "wins"; for all reasonable (and quite - // a few unreasonable) interest rates, the lower-order terms contribute the - // most to the total. The following table lists some of the calculated - // approximations for different values of n, along with the "true" result: - // - // Total: 10% 20% 50% 100% 200% 500% 1000% - // ----------------------------------------------------------------------- - // n = 1: 10.0% 20.0% 50.0% 100.0% 200.0% 500.0% 1000.0% - // n = 2: 10.5% 22.0% 62.5% 150.0% 400.0% 1750.0% 6000.0% - // n = 3: 10.5% 22.1% 64.6% 166.7% 533.3% 3833.3% 22666.7% - // n = 4: 10.5% 22.1% 64.8% 170.8% 600.0% 6437.5% 64333.3% - // n = 5: 10.5% 22.1% 64.9% 171.7% 626.7% 9041.7% 147666.7% - // n = 6: 10.5% 22.1% 64.9% 171.8% 635.6% 11211.8% 286555.6% - // n = 7: 10.5% 22.1% 64.9% 171.8% 638.1% 12761.9% 484968.3% - // n = 8: 10.5% 22.1% 64.9% 171.8% 638.7% 13730.7% 732984.1% - // n = 9: 10.5% 22.1% 64.9% 171.8% 638.9% 14268.9% 1008557.3% - // n = 10: 10.5% 22.1% 64.9% 171.8% 638.9% 14538.1% 1284130.5% - // - // (n=Infinity): 10.5% 22.1% 64.9% 171.8% 638.9% 14741.3% 2202546.6% - // - // For instance, calculating the compounding effects of 200% in "total" - // interest to the sixth order results in 635.6%, whereas the true result - // is 638.9%. - // At 500% that difference is a little more dramatic, but it is still in - // the same ballpark -- and of little practical consequence unless the - // collateral can be expected to go up more than 112 times in value. - // Still, for volatile tokens, or an asset that is somehow known to be very - // inflationary, use a different number. - // Zero (no interest at all) is ignored and treated as one (linear only). - uint8 private constant COMPOUND_INTEREST_TERMS = 6; - - // For signed lend / borrow requests: - mapping(address => uint256) public nonces; - - /// @notice The constructor is only used for the initial master contract. - /// @notice Subsequent clones are initialised via `init`. - constructor(IBentoBoxV1 bentoBox_) public { - bentoBox = bentoBox_; - masterContract = this; - } - - /// @notice De facto constructor for clone contracts - function init(bytes calldata data) public payable override { - require(address(collateral) == address(0), "NFTPair: already initialized"); - (collateral, asset) = abi.decode(data, (IERC721, IERC20)); - require(address(collateral) != address(0), "NFTPair: bad pair"); - } - - function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { - TokenLoan memory loan = tokenLoan[tokenId]; - if (loan.status == LOAN_OUTSTANDING) { - // The lender can change terms so long as the changes are strictly - // the same or better for the borrower: - require(msg.sender == loan.lender, "NFTPair: not the lender"); - TokenLoanParams memory cur = tokenLoanParams[tokenId]; - require( - params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS, - "NFTPair: worse params" - ); - } else if (loan.status == LOAN_REQUESTED) { - // The borrower has already deposited the collateral and can - // change whatever they like - require(msg.sender == loan.borrower, "NFTPair: not the borrower"); - } else { - // The loan has not been taken out yet; the borrower needs to - // provide collateral. - revert("NFTPair: no collateral"); - } - tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); - } - - function _requestLoan( - address collateralProvider, - uint256 tokenId, - TokenLoanParams memory params, - address to, - bool skim - ) private { - // Edge case: valuation can be zero. That effectively gifts the NFT and - // is therefore a bad idea, but does not break the contract. - require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); - if (skim) { - require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); - } else { - collateral.transferFrom(collateralProvider, address(this), tokenId); - } - TokenLoan memory loan; - loan.borrower = to; - loan.status = LOAN_REQUESTED; - tokenLoan[tokenId] = loan; - tokenLoanParams[tokenId] = params; - - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); - } - - /// @notice Deposit an NFT as collateral and request a loan against it - /// @param tokenId ID of the NFT - /// @param to Address to receive the loan, or option to withdraw collateral - /// @param params Loan conditions on offer - /// @param skim True if the token has already been transfered - function requestLoan( - uint256 tokenId, - TokenLoanParams memory params, - address to, - bool skim - ) public { - _requestLoan(msg.sender, tokenId, params, to, skim); - } - - /// @notice Removes `tokenId` as collateral and transfers it to `to`. - /// @notice This destroys the loan. - /// @param tokenId The token - /// @param to The receiver of the token. - function removeCollateral(uint256 tokenId, address to) public { - TokenLoan memory loan = tokenLoan[tokenId]; - if (loan.status == LOAN_REQUESTED) { - // We are withdrawing collateral that is not in use: - require(msg.sender == loan.borrower, "NFTPair: not the borrower"); - } else if (loan.status == LOAN_OUTSTANDING) { - // We are seizing collateral as the lender. The loan has to be - // expired and not paid off: - require(msg.sender == loan.lender, "NFTPair: not the lender"); - - if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { - TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - // No underflow: loan.startTime is only ever set to a block timestamp - // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(loanParams.valuation, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); - uint256 amount = loanParams.valuation + interest; - (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); - require (rate.mul(loanParams.ltv) / BPS < amount, "NFT is still valued"); - } - } - // If there somehow is collateral but no accompanying loan, then anyone - // can claim it by first requesting a loan with `skim` set to true, and - // then withdrawing. So we might as well allow it here.. - delete tokenLoan[tokenId]; - collateral.transferFrom(address(this), to, tokenId); - emit LogRemoveCollateral(tokenId, to); - } - - // Assumes the lender has agreed to the loan. - function _lend( - address lender, - uint256 tokenId, - TokenLoanParams memory accepted, - bool skim - ) internal { - TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); - TokenLoanParams memory params = tokenLoanParams[tokenId]; - - // Valuation has to be an exact match, everything else must be at least - // as good for the lender as `accepted`. - require( - params.valuation == accepted.valuation && - params.duration <= accepted.duration && - params.annualInterestBPS >= accepted.annualInterestBPS, - "NFTPair: bad params" - ); - - if (params.oracle != INFTOracle(0)) { - (, uint256 rate) = params.oracle.get(address(this), tokenId); - require(rate.mul(uint256(params.ltv)) / BPS >= params.valuation, "Oracle: price too low."); - } - - uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); - // No overflow: at most 128 + 16 bits (fits in BentoBox) - uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; - uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; - - if (skim) { - require( - bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), - "NFTPair: skim too much" - ); - } else { - bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); - } - // No underflow: follows from OPEN_FEE_BPS <= BPS - uint256 borrowerShare = totalShare - openFeeShare; - bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); - // No overflow: addends (and result) must fit in BentoBox - feesEarnedShare += protocolFeeShare; - - loan.lender = lender; - loan.status = LOAN_OUTSTANDING; - loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. - tokenLoan[tokenId] = loan; - - emit LogLend(lender, tokenId); - } - - /// @notice Lends with the parameters specified by the borrower. - /// @param tokenId ID of the token that will function as collateral - /// @param accepted Loan parameters as the lender saw them, for security - /// @param skim True if the funds have been transfered to the contract - function lend( - uint256 tokenId, - TokenLoanParams memory accepted, - bool skim - ) public { - _lend(msg.sender, tokenId, accepted, skim); - } - - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparator(); - } - - // NOTE on signature hashes: the domain separator only guarantees that the - // chain ID and master contract are a match, so we explicitly include the - // clone address (and the asset/collateral addresses): - - // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; - - // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336; - - /// @notice Request and immediately borrow from a pre-committed lender - - /// @notice Caller provides collateral; loan can go to a different address. - /// @param tokenId ID of the token that will function as collateral - /// @param lender Lender, whose BentoBox balance the funds will come from - /// @param recipient Address to receive the loan. - /// @param params Loan parameters requested, and signed by the lender - /// @param skimCollateral True if the collateral has already been transfered - /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. - function requestAndBorrow( - uint256 tokenId, - address lender, - address recipient, - TokenLoanParams memory params, - bool skimCollateral, - bool anyTokenId, - SignatureParams memory signature - ) public { - if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); - } else { - require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); - uint256 nonce = nonces[lender]++; - bytes32 dataHash = keccak256( - abi.encode( - LEND_SIGNATURE_HASH, - address(this), - anyTokenId ? 0 : tokenId, - anyTokenId, - params.valuation, - params.duration, - params.annualInterestBPS, - params.ltv, - params.oracle, - nonce, - signature.deadline - ) - ); - require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == lender, "NFTPair: signature invalid"); - } - _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); - _lend(lender, tokenId, params, false); - } - - /// @notice Take collateral from a pre-commited borrower and lend against it - /// @notice Collateral must come from the borrower, not a third party. - /// @param tokenId ID of the token that will function as collateral - /// @param borrower Address that provides collateral and receives the loan - /// @param params Loan terms offered, and signed by the borrower - /// @param skimFunds True if the funds have been transfered to the contract - function takeCollateralAndLend( - uint256 tokenId, - address borrower, - TokenLoanParams memory params, - bool skimFunds, - SignatureParams memory signature - ) public { - require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); - uint256 nonce = nonces[borrower]++; - bytes32 dataHash = keccak256( - abi.encode( - BORROW_SIGNATURE_HASH, - address(this), - tokenId, - params.valuation, - params.duration, - params.annualInterestBPS, - params.ltv, - params.oracle, - nonce, - signature.deadline - ) - ); - require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == borrower, "NFTPair: signature invalid"); - _requestLoan(borrower, tokenId, params, borrower, false); - _lend(msg.sender, tokenId, params, skimFunds); - } - - /// Approximates continuous compounding. Uses Horner's method to evaluate - /// the truncated Maclaurin series for exp - 1, accumulating rounding - /// errors along the way. The following is always guaranteed: - /// - /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), - /// - /// where time = t/YEAR, up to at most the rounding error obtained in - /// calculating linear interest. - /// - /// If the theoretical result that we are approximating (the rightmost part - /// of the above inquality) fits in 128 bits, then the function is - /// guaranteed not to revert (unless n > 250, which is way too high). - /// If even the linear interest (leftmost part of the inequality) does not - /// the function will revert. - /// Otherwise, the function may revert, return a reasonable result, or - /// return a very inaccurate result. Even then the above inequality is - /// respected. - function calculateInterest( - uint256 principal, - uint64 t, - uint16 aprBPS - ) public pure returns (uint256 interest) { - // (NOTE: n is hardcoded as COMPOUND_INTEREST_TERMS) - // - // We calculate - // - // ----- n ----- n - // \ principal * (t * aprBPS)^k \ - // ) -------------------------- =: ) term_k - // / k! * YEAR_BPS^k / - // ----- k = 1 ----- k = 1 - // - // which approaches, but never exceeds the "theoretical" result, - // - // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 - // - // as n goes to infinity. We use the fact that - // - // principal * (t * aprBPS)^(k-1) * (t * aprBPS) - // term_k = --------------------------------------------- - // (k-1)! * k * YEAR_BPS^(k-1) * YEAR_BPS - // - // t * aprBPS - // = term_{k-1} * ------------ (*) - // k * YEAR_BPS - // - // to calculate the terms one by one. The principal affords us the - // precision to carry out the division without resorting to fixed-point - // math. Any rounding error is downward, which we consider acceptable. - // - // Since all numbers involved are positive, each term is certainly - // bounded above by M. From (*) we see that any intermediate results - // are at most - // - // denom_k := k * YEAR_BPS. - // - // times M. Since YEAR_BPS fits in 38 bits, denom_k fits in 46 bits, - // which proves that all calculations will certainly not overflow if M - // fits in 128 bits. - // - // If M does not fit, then the intermediate results for some term may - // eventually overflow, but this cannot happen at the first term, and - // neither can the total overflow because it uses checked math. - // - // This constitutes a guarantee of specified behavior when M >= 2^128. - uint256 x = uint256(t) * aprBPS; - uint256 term_k = (principal * x) / YEAR_BPS; - uint256 denom_k = YEAR_BPS; - - interest = term_k; - for (uint256 k = 2; k <= COMPOUND_INTEREST_TERMS; k++) { - denom_k += YEAR_BPS; - term_k = (term_k * x) / denom_k; - interest = interest.add(term_k); // <- Only overflow check we need - } - - if (interest >= 2**128) { - revert(); - } - } - - function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { - TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); - TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; - require( - // Addition is safe: both summands are smaller than 256 bits - uint256(loan.startTime) + loanParams.duration > block.timestamp, - "NFTPair: loan expired" - ); - - uint128 principal = loanParams.valuation; - - // No underflow: loan.startTime is only ever set to a block timestamp - // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); - uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; - amount = principal + interest; - - uint256 totalShare = bentoBox.toShare(asset, amount, false); - uint256 feeShare = bentoBox.toShare(asset, fee, false); - - address from; - if (skim) { - require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); - from = address(this); - // No overflow: result fits in BentoBox - } else { - bentoBox.transfer(asset, msg.sender, address(this), feeShare); - from = msg.sender; - } - // No underflow: PROTOCOL_FEE_BPS < BPS by construction. - feesEarnedShare += feeShare; - delete tokenLoan[tokenId]; - - bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); - collateral.transferFrom(address(this), loan.borrower, tokenId); - - emit LogRepay(from, tokenId); - } - - uint8 internal constant ACTION_REPAY = 2; - uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; - - uint8 internal constant ACTION_REQUEST_LOAN = 12; - uint8 internal constant ACTION_LEND = 13; - - // Function on BentoBox - uint8 internal constant ACTION_BENTO_DEPOSIT = 20; - uint8 internal constant ACTION_BENTO_WITHDRAW = 21; - uint8 internal constant ACTION_BENTO_TRANSFER = 22; - uint8 internal constant ACTION_BENTO_TRANSFER_MULTIPLE = 23; - uint8 internal constant ACTION_BENTO_SETAPPROVAL = 24; - - // Any external call (except to BentoBox) - uint8 internal constant ACTION_CALL = 30; - - // Signed requests - uint8 internal constant ACTION_REQUEST_AND_BORROW = 40; - uint8 internal constant ACTION_TAKE_COLLATERAL_AND_LEND = 41; - - int256 internal constant USE_VALUE1 = -1; - int256 internal constant USE_VALUE2 = -2; - - /// @dev Helper function for choosing the correct value (`value1` or `value2`) depending on `inNum`. - function _num( - int256 inNum, - uint256 value1, - uint256 value2 - ) internal pure returns (uint256 outNum) { - outNum = inNum >= 0 ? uint256(inNum) : (inNum == USE_VALUE1 ? value1 : value2); - } - - /// @dev Helper function for depositing into `bentoBox`. - function _bentoDeposit( - bytes memory data, - uint256 value, - uint256 value1, - uint256 value2 - ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); - amount = int256(_num(amount, value1, value2)); // Done this way to avoid stack too deep errors - share = int256(_num(share, value1, value2)); - return bentoBox.deposit{value: value}(token, msg.sender, to, uint256(amount), uint256(share)); - } - - /// @dev Helper function to withdraw from the `bentoBox`. - function _bentoWithdraw( - bytes memory data, - uint256 value1, - uint256 value2 - ) internal returns (uint256, uint256) { - (IERC20 token, address to, int256 amount, int256 share) = abi.decode(data, (IERC20, address, int256, int256)); - return bentoBox.withdraw(token, msg.sender, to, _num(amount, value1, value2), _num(share, value1, value2)); - } - - /// @dev Helper function to perform a contract call and eventually extracting revert messages on failure. - /// Calls to `bentoBox` or `collateral` are not allowed for security reasons. - /// This also means that calls made from this contract shall *not* be trusted. - function _call( - uint256 value, - bytes memory data, - uint256 value1, - uint256 value2 - ) internal returns (bytes memory, uint8) { - (address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) = abi.decode( - data, - (address, bytes, bool, bool, uint8) - ); - - if (useValue1 && !useValue2) { - callData = abi.encodePacked(callData, value1); - } else if (!useValue1 && useValue2) { - callData = abi.encodePacked(callData, value2); - } else if (useValue1 && useValue2) { - callData = abi.encodePacked(callData, value1, value2); - } - - require(callee != address(bentoBox) && callee != address(collateral) && callee != address(this), "NFTPair: can't call"); - - (bool success, bytes memory returnData) = callee.call{value: value}(callData); - require(success, "NFTPair: call failed"); - return (returnData, returnValues); - } - - /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. - /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). - /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. - /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. - /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. - /// @return value1 May contain the first positioned return value of the last executed action (if applicable). - /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). - function cook( - uint8[] calldata actions, - uint256[] calldata values, - bytes[] calldata datas - ) external payable returns (uint256 value1, uint256 value2) { - for (uint256 i = 0; i < actions.length; i++) { - uint8 action = actions[i]; - if (action == ACTION_REPAY) { - (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); - repay(tokenId, skim); - } else if (action == ACTION_REMOVE_COLLATERAL) { - (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); - removeCollateral(tokenId, to); - } else if (action == ACTION_REQUEST_LOAN) { - (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode( - datas[i], - (uint256, TokenLoanParams, address, bool) - ); - requestLoan(tokenId, params, to, skim); - } else if (action == ACTION_LEND) { - (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool)); - lend(tokenId, params, skim); - } else if (action == ACTION_BENTO_SETAPPROVAL) { - (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( - datas[i], - (address, address, bool, uint8, bytes32, bytes32) - ); - bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); - } else if (action == ACTION_BENTO_DEPOSIT) { - (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); - } else if (action == ACTION_BENTO_WITHDRAW) { - (value1, value2) = _bentoWithdraw(datas[i], value1, value2); - } else if (action == ACTION_BENTO_TRANSFER) { - (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); - bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); - } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { - (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); - bentoBox.transferMultiple(token, msg.sender, tos, shares); - } else if (action == ACTION_CALL) { - (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); - - if (returnValues == 1) { - (value1) = abi.decode(returnData, (uint256)); - } else if (returnValues == 2) { - (value1, value2) = abi.decode(returnData, (uint256, uint256)); - } - } else if (action == ACTION_REQUEST_AND_BORROW) { - ( - uint256 tokenId, - address lender, - address recipient, - TokenLoanParams memory params, - bool skimCollateral, - bool anyTokenId, - SignatureParams memory signature - ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams)); - requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); - } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { - ( - uint256 tokenId, - address borrower, - TokenLoanParams memory params, - bool skimFunds, - SignatureParams memory signature - ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); - takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); - } - } - } - - /// @notice Withdraws the fees accumulated. - function withdrawFees() public { - address to = masterContract.feeTo(); - - uint256 _share = feesEarnedShare; - if (_share > 0) { - bentoBox.transfer(asset, address(this), to, _share); - feesEarnedShare = 0; - } - - emit LogWithdrawFees(to, _share); - } - - /// @notice Sets the beneficiary of fees accrued in liquidations. - /// MasterContract Only Admin function. - /// @param newFeeTo The address of the receiver. - function setFeeTo(address newFeeTo) public onlyOwner { - feeTo = newFeeTo; - emit LogFeeTo(newFeeTo); - } -} From ae47d036c22809bbd6833a272a455a9c61fbae7a Mon Sep 17 00:00:00 2001 From: 0xMerlin <83640350+0xm3rlin@users.noreply.github.com> Date: Tue, 26 Apr 2022 11:05:54 -0400 Subject: [PATCH 073/107] Changed Removal to allow liquidation from other addresses --- contracts/NFTPair.sol | 2 +- contracts/NFTPairWithOracle.sol | 6 +++--- test/NFTPair.test.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index bc2b74ce..54968683 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -252,7 +252,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } else if (loan.status == LOAN_OUTSTANDING) { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: - require(msg.sender == loan.lender, "NFTPair: not the lender"); + require(to == loan.lender, "NFTPair: not the lender"); require( // Addition is safe: both summands are smaller than 256 bits uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp, diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 1bb89e94..2aeb4fd9 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -84,7 +84,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // Immutables (for MasterContract and all clones) IBentoBoxV1 public immutable bentoBox; - NFTPair public immutable masterContract; + NFTPairWithOracle public immutable masterContract; // MasterContract variables address public feeTo; @@ -260,9 +260,9 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // We are withdrawing collateral that is not in use: require(msg.sender == loan.borrower, "NFTPair: not the borrower"); } else if (loan.status == LOAN_OUTSTANDING) { - // We are seizing collateral as the lender. The loan has to be + // We are seizing collateral towards the lender. The loan has to be // expired and not paid off: - require(msg.sender == loan.lender, "NFTPair: not the lender"); + require(to == loan.lender, "NFTPair: not the lender"); if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index c1ae3f0e..ff5d351c 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -550,16 +550,16 @@ describe("NFT Pair", async () => { it("Should allow lenders to seize collateral upon expiry", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); // Send it to someone else for a change: - await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)) + await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)) .to.emit(pair, "LogRemoveCollateral") - .withArgs(apeIds.aliceOne, carol.address) + .withArgs(apeIds.aliceOne, bob.address) .to.emit(apes, "Transfer") - .withArgs(pair.address, carol.address, apeIds.aliceOne); + .withArgs(pair.address, bob.address, apeIds.aliceOne); }); it("Should not allow lenders to seize collateral otherwise", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); - await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not expired"); + await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)).to.be.revertedWith("NFTPair: not expired"); }); it("Should not allow others to seize collateral ever", async () => { From a0a802bf652d149527ce93c7a18a234864c2290a Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 20:59:49 +0200 Subject: [PATCH 074/107] More thoroughly check for expiration logic --- test/NFTPair.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index ff5d351c..0912ec88 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -547,7 +547,7 @@ describe("NFT Pair", async () => { await expect(pair.connect(alice).removeCollateral(apeIds.aliceOne, alice.address)).to.be.revertedWith("NFTPair: not the lender"); }); - it("Should allow lenders to seize collateral upon expiry", async () => { + it("Should allow lenders to seize collateral at expiry", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); // Send it to someone else for a change: await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)) @@ -557,6 +557,16 @@ describe("NFT Pair", async () => { .withArgs(pair.address, bob.address, apeIds.aliceOne); }); + it("Should allow lenders to seize collateral after expiry", async () => { + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); + // Send it to someone else for a change: + await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)) + .to.emit(pair, "LogRemoveCollateral") + .withArgs(apeIds.aliceOne, bob.address) + .to.emit(apes, "Transfer") + .withArgs(pair.address, bob.address, apeIds.aliceOne); + }); + it("Should not allow lenders to seize collateral otherwise", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)).to.be.revertedWith("NFTPair: not expired"); From 0af7ea3bfe58b022cace40491eb147653b7c9cf1 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 21:26:28 +0200 Subject: [PATCH 075/107] (Comments / auto-formatted line endings) --- contracts/NFTPairWithOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 2aeb4fd9..dccb1bf2 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -32,7 +32,7 @@ import "./interfaces/INFTOracle.sol"; struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 duration; // Length of loan in seconds - uint16 ltv; // + uint16 ltv; // uint16 annualInterestBPS; // Variable cost of taking out the loan INFTOracle oracle; // oracle used } @@ -261,7 +261,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.borrower, "NFTPair: not the borrower"); } else if (loan.status == LOAN_OUTSTANDING) { // We are seizing collateral towards the lender. The loan has to be - // expired and not paid off: + // expired and not paid off, or underwater and not paid off: require(to == loan.lender, "NFTPair: not the lender"); if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { From aa2ff04dab5051df60c25e83ee420fd4326709fc Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 21:37:36 +0200 Subject: [PATCH 076/107] Add LTV to relevant checks / parameter lists --- contracts/NFTPairWithOracle.sol | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index dccb1bf2..6f92d7e3 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -32,8 +32,8 @@ import "./interfaces/INFTOracle.sol"; struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 duration; // Length of loan in seconds - uint16 ltv; // uint16 annualInterestBPS; // Variable cost of taking out the loan + uint16 ltv; // Required to avoid liquidation INFTOracle oracle; // oracle used } @@ -72,8 +72,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltv); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltv); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request @@ -196,7 +196,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( - params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS, + params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltv <= cur.ltv, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -209,7 +209,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); + emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltv); } function _requestLoan( @@ -233,7 +233,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { tokenLoan[tokenId] = loan; tokenLoanParams[tokenId] = params; - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltv); } /// @notice Deposit an NFT as collateral and request a loan against it @@ -298,7 +298,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require( params.valuation == accepted.valuation && params.duration <= accepted.duration && - params.annualInterestBPS >= accepted.annualInterestBPS, + params.annualInterestBPS >= accepted.annualInterestBPS && + params.ltv >= accepted.ltv, "NFTPair: bad params" ); From 9d59ccf5cbf29d4280576470cc43b7ac5c5ec85d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 21:41:44 +0200 Subject: [PATCH 077/107] Rename ltv => ltvBPS --- contracts/NFTPairWithOracle.sol | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 6f92d7e3..e924b0fc 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -33,7 +33,7 @@ struct TokenLoanParams { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 duration; // Length of loan in seconds uint16 annualInterestBPS; // Variable cost of taking out the loan - uint16 ltv; // Required to avoid liquidation + uint16 ltvBPS; // Required to avoid liquidation INFTOracle oracle; // oracle used } @@ -72,8 +72,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltv); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltv); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltvBPS); + event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltvBPS); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request @@ -196,7 +196,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( - params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltv <= cur.ltv, + params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS <= cur.ltvBPS, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -209,7 +209,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltv); + emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltvBPS); } function _requestLoan( @@ -233,7 +233,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { tokenLoan[tokenId] = loan; tokenLoanParams[tokenId] = params; - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltv); + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltvBPS); } /// @notice Deposit an NFT as collateral and request a loan against it @@ -271,7 +271,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 interest = calculateInterest(loanParams.valuation, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); uint256 amount = loanParams.valuation + interest; (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); - require (rate.mul(loanParams.ltv) / BPS < amount, "NFT is still valued"); + require (rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued"); } } // If there somehow is collateral but no accompanying loan, then anyone @@ -299,13 +299,13 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && - params.ltv >= accepted.ltv, + params.ltvBPS >= accepted.ltvBPS, "NFTPair: bad params" ); if (params.oracle != INFTOracle(0)) { (, uint256 rate) = params.oracle.get(address(this), tokenId); - require(rate.mul(uint256(params.ltv)) / BPS >= params.valuation, "Oracle: price too low."); + require(rate.mul(uint256(params.ltvBPS)) / BPS >= params.valuation, "Oracle: price too low."); } uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); @@ -394,7 +394,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.valuation, params.duration, params.annualInterestBPS, - params.ltv, + params.ltvBPS, params.oracle, nonce, signature.deadline @@ -429,7 +429,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.valuation, params.duration, params.annualInterestBPS, - params.ltv, + params.ltvBPS, params.oracle, nonce, signature.deadline From 741a255501d8597da9acc3ba38daf2069426f12a Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 21:42:26 +0200 Subject: [PATCH 078/107] Formatting --- contracts/NFTPairWithOracle.sol | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index e924b0fc..c574ce2a 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -72,7 +72,14 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltvBPS); + event LogRequestLoan( + address indexed borrower, + uint256 indexed tokenId, + uint128 valuation, + uint64 duration, + uint16 annualInterestBPS, + uint16 ltvBPS + ); event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltvBPS); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); @@ -196,7 +203,10 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(msg.sender == loan.lender, "NFTPair: not the lender"); TokenLoanParams memory cur = tokenLoanParams[tokenId]; require( - params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && params.ltvBPS <= cur.ltvBPS, + params.duration >= cur.duration && + params.valuation <= cur.valuation && + params.annualInterestBPS <= cur.annualInterestBPS && + params.ltvBPS <= cur.ltvBPS, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -268,10 +278,14 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; // No underflow: loan.startTime is only ever set to a block timestamp // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(loanParams.valuation, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 interest = calculateInterest( + loanParams.valuation, + uint64(block.timestamp - loan.startTime), + loanParams.annualInterestBPS + ).to128(); uint256 amount = loanParams.valuation + interest; (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); - require (rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued"); + require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued"); } } // If there somehow is collateral but no accompanying loan, then anyone @@ -710,13 +724,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams)); requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { - ( - uint256 tokenId, - address borrower, - TokenLoanParams memory params, - bool skimFunds, - SignatureParams memory signature - ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); + (uint256 tokenId, address borrower, TokenLoanParams memory params, bool skimFunds, SignatureParams memory signature) = abi + .decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); } } From 5f6f3020d3262f26d4e9aa935690f13cda51de32 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 26 Apr 2022 21:45:43 +0200 Subject: [PATCH 079/107] Update signature hashes --- contracts/NFTPairWithOracle.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index c574ce2a..4ce10bdb 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -370,11 +370,11 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // chain ID and master contract are a match, so we explicitly include the // clone address (and the asset/collateral addresses): - // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; + // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint16 ltvBPS,address oracle,uint256 nonce,uint256 deadline)") + bytes32 private constant LEND_SIGNATURE_HASH = 0x4bfd5d24664945f4bb81f6061bd624907d74ba338190bdd6aa37f65838a8a533; - // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") - bytes32 private constant BORROW_SIGNATURE_HASH = 0xf2c9128b0fb8406af3168320897e5ff08f3bb536dd5f804c29ed276e93ec4336; + // keccak256("Borrow(address contract,uint256 tokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint16 ltvBPS,address oracle,uint256 nonce,uint256 deadline)") + bytes32 private constant BORROW_SIGNATURE_HASH = 0xfc58c7a8ea6a96e25d218e36759058a704bbf0bebb53a109a44ca82f025cb769; /// @notice Request and immediately borrow from a pre-committed lender From 7e3a3834c6944fdbfdfaaad9ccc49e7dab0bd338 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 29 Apr 2022 17:46:11 +0200 Subject: [PATCH 080/107] Fix LTV parameter comparison check --- contracts/NFTPairWithOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 4ce10bdb..1a58c9c5 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -206,7 +206,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && - params.ltvBPS <= cur.ltvBPS, + params.ltvBPS >= cur.ltvBPS, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -313,7 +313,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && - params.ltvBPS >= accepted.ltvBPS, + params.ltvBPS <= accepted.ltvBPS, "NFTPair: bad params" ); From ba4b627a24f5d9b41165c36545e7528aba9b025d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 1 May 2022 00:09:49 +0200 Subject: [PATCH 081/107] Enforce that the oracle cannot be changed --- contracts/NFTPairWithOracle.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 1a58c9c5..e0d16d8a 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -206,7 +206,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.duration >= cur.duration && params.valuation <= cur.valuation && params.annualInterestBPS <= cur.annualInterestBPS && - params.ltvBPS >= cur.ltvBPS, + params.ltvBPS >= cur.ltvBPS && + params.oracle == cur.oracle, "NFTPair: worse params" ); } else if (loan.status == LOAN_REQUESTED) { @@ -313,7 +314,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { params.valuation == accepted.valuation && params.duration <= accepted.duration && params.annualInterestBPS >= accepted.annualInterestBPS && - params.ltvBPS <= accepted.ltvBPS, + params.ltvBPS <= accepted.ltvBPS && + params.oracle == accepted.oracle, "NFTPair: bad params" ); From d15cad3fd784e606e2056755fd131d9139e6a555 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 1 May 2022 00:31:47 +0200 Subject: [PATCH 082/107] Add cook test scenario for BentoBox transfers --- test/NFTPair.test.ts | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 0912ec88..9a09ef03 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -74,6 +74,7 @@ const nextDecade = Math.floor(new Date().getTime() / 1000) + YEAR * 10; describe("NFT Pair", async () => { let apes: ERC721Mock; let guineas: ERC20Mock; + let weth: WETH9Mock; let bentoBox: BentoBoxMock; let masterContract: NFTPair; let deployer: SignerWithAddress; @@ -129,7 +130,11 @@ describe("NFT Pair", async () => { const mintApe = (ownerAddress) => mintToken(apes, ownerAddress); before(async () => { - const weth = await deployContract("WETH9Mock"); + weth = await deployContract("WETH9Mock"); + // The BentoBox complains if totalSupply = 0, and total supply is however + // many ETH has been deposited: + await weth.deposit({ value: getBigNumber(1) }); + bentoBox = await deployContract("BentoBoxMock", weth.address); masterContract = await deployContract("NFTPair", bentoBox.address); await bentoBox.whitelistMasterContract(masterContract.address, true); @@ -1515,7 +1520,7 @@ describe("NFT Pair", async () => { expect(n).to.be.gte(2); const amountNeeded = getBigNumber(n * (n + 1) * 6); actions.push(ACTION_BENTO_DEPOSIT); - values.push(0); // TODO: Test with ETH as the token + values.push(0); datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, alice.address, amountNeeded, 0])); // 3. Lend @@ -1650,6 +1655,43 @@ describe("NFT Pair", async () => { ]) ).to.be.revertedWith("NFTPair: bad params"); }); + + it("Should allow multiple BentoBox transfers", async () => { + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // Share/amount ratio is 1: + expect(await bentoBox.toShare(weth.address, getBigNumber(1), false)).to.equal(getBigNumber(1)); + + // 1. Deposit + const toBob = getBigNumber(1); + const toCarol = getBigNumber(2); + const total = toBob.add(toCarol); + const USE_ETHEREUM = AddressZero; + actions.push(ACTION_BENTO_DEPOSIT); + values.push(total); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [USE_ETHEREUM, alice.address, total, 0])); + + // 2. Transfer (single) + const toCarolSingle = toCarol.mul(1).div(4); + const toCarolBatch = toCarol.sub(toCarolSingle); + expect(toCarolSingle.mul(toCarolBatch)).to.be.gt(0); + actions.push(ACTION_BENTO_TRANSFER); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256"], [weth.address, carol.address, toCarolSingle])); + + // 3. Transfer (multiple) + actions.push(ACTION_BENTO_TRANSFER_MULTIPLE); + values.push(0); + datas.push(encodeParameters(["address", "address[]", "uint256[]"], [weth.address, [bob.address, carol.address], [toBob, toCarolBatch]])); + + await pair.connect(alice).cook(actions, values, datas, { value: total }); + + expect(await bentoBox.balanceOf(weth.address, alice.address)).to.equal(0); + expect(await bentoBox.balanceOf(weth.address, bob.address)).to.equal(toBob); + expect(await bentoBox.balanceOf(weth.address, carol.address)).to.equal(toCarol); + }); }); describeSnapshot("Lending Club", () => { From d05d931fe09d01f9dd5d980e43869f2e3beda955 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 27 May 2022 17:02:01 +0200 Subject: [PATCH 083/107] Allow the lender to "liquidate" to a different address --- contracts/NFTPair.sol | 2 +- contracts/NFTPairWithOracle.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 54968683..af007084 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -252,7 +252,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } else if (loan.status == LOAN_OUTSTANDING) { // We are seizing collateral as the lender. The loan has to be // expired and not paid off: - require(to == loan.lender, "NFTPair: not the lender"); + require(to == loan.lender || msg.sender == loan.lender, "NFTPair: not the lender"); require( // Addition is safe: both summands are smaller than 256 bits uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp, diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index e0d16d8a..d163e47e 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -273,7 +273,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } else if (loan.status == LOAN_OUTSTANDING) { // We are seizing collateral towards the lender. The loan has to be // expired and not paid off, or underwater and not paid off: - require(to == loan.lender, "NFTPair: not the lender"); + require(to == loan.lender || msg.sender == loan.lender, "NFTPair: not the lender"); if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; From e179cd4c49b83f4e0c2a4bb97df857be09dae2b6 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 27 May 2022 17:02:05 +0200 Subject: [PATCH 084/107] Deduplicate names in "WithOracle" contract --- contracts/NFTPairWithOracle.sol | 68 +++++++++++++++---------------- contracts/interfaces/INFTPair.sol | 19 +++++++++ 2 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 contracts/interfaces/INFTPair.sol diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index d163e47e..d4896909 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -29,7 +29,7 @@ import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; import "./interfaces/INFTOracle.sol"; -struct TokenLoanParams { +struct TokenLoanParamsWithOracle { uint128 valuation; // How much will you get? OK to owe until expiration. uint64 duration; // Length of loan in seconds uint16 annualInterestBPS; // Variable cost of taking out the loan @@ -44,23 +44,11 @@ struct SignatureParams { bytes32 s; } -interface ILendingClub { +interface ILendingClubWithOracle { // Per token settings. - function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); + function willLend(uint256 tokenId, TokenLoanParamsWithOracle memory params) external view returns (bool); - function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); -} - -interface INFTPair { - function collateral() external view returns (IERC721); - - function asset() external view returns (IERC20); - - function masterContract() external view returns (address); - - function bentoBox() external view returns (IBentoBoxV1); - - function removeCollateral(uint256 tokenId, address to) external; + function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParamsWithOracle memory); } /// @title NFTPairWithOracle @@ -108,7 +96,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 public feesEarnedShare; // Per token settings. - mapping(uint256 => TokenLoanParams) public tokenLoanParams; + mapping(uint256 => TokenLoanParamsWithOracle) public tokenLoanParams; uint8 private constant LOAN_INITIAL = 0; uint8 private constant LOAN_REQUESTED = 1; @@ -195,13 +183,13 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(address(collateral) != address(0), "NFTPair: bad pair"); } - function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { + function updateLoanParams(uint256 tokenId, TokenLoanParamsWithOracle memory params) public { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { // The lender can change terms so long as the changes are strictly // the same or better for the borrower: require(msg.sender == loan.lender, "NFTPair: not the lender"); - TokenLoanParams memory cur = tokenLoanParams[tokenId]; + TokenLoanParamsWithOracle memory cur = tokenLoanParams[tokenId]; require( params.duration >= cur.duration && params.valuation <= cur.valuation && @@ -226,7 +214,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { function _requestLoan( address collateralProvider, uint256 tokenId, - TokenLoanParams memory params, + TokenLoanParamsWithOracle memory params, address to, bool skim ) private { @@ -254,7 +242,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { /// @param skim True if the token has already been transfered function requestLoan( uint256 tokenId, - TokenLoanParams memory params, + TokenLoanParamsWithOracle memory params, address to, bool skim ) public { @@ -276,7 +264,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { require(to == loan.lender || msg.sender == loan.lender, "NFTPair: not the lender"); if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { - TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + TokenLoanParamsWithOracle memory loanParams = tokenLoanParams[tokenId]; // No underflow: loan.startTime is only ever set to a block timestamp // Cast is safe: if this overflows, then all loans have expired anyway uint256 interest = calculateInterest( @@ -301,12 +289,12 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { function _lend( address lender, uint256 tokenId, - TokenLoanParams memory accepted, + TokenLoanParamsWithOracle memory accepted, bool skim ) internal { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); - TokenLoanParams memory params = tokenLoanParams[tokenId]; + TokenLoanParamsWithOracle memory params = tokenLoanParams[tokenId]; // Valuation has to be an exact match, everything else must be at least // as good for the lender as `accepted`. @@ -357,7 +345,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { /// @param skim True if the funds have been transfered to the contract function lend( uint256 tokenId, - TokenLoanParams memory accepted, + TokenLoanParamsWithOracle memory accepted, bool skim ) public { _lend(msg.sender, tokenId, accepted, skim); @@ -391,13 +379,13 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 tokenId, address lender, address recipient, - TokenLoanParams memory params, + TokenLoanParamsWithOracle memory params, bool skimCollateral, bool anyTokenId, SignatureParams memory signature ) public { if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); + require(ILendingClubWithOracle(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); } else { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; @@ -431,7 +419,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { function takeCollateralAndLend( uint256 tokenId, address borrower, - TokenLoanParams memory params, + TokenLoanParamsWithOracle memory params, bool skimFunds, SignatureParams memory signature ) public { @@ -540,7 +528,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); - TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; + TokenLoanParamsWithOracle memory loanParams = tokenLoanParams[tokenId]; require( // Addition is safe: both summands are smaller than 256 bits uint256(loan.startTime) + loanParams.duration > block.timestamp, @@ -682,13 +670,16 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); } else if (action == ACTION_REQUEST_LOAN) { - (uint256 tokenId, TokenLoanParams memory params, address to, bool skim) = abi.decode( + (uint256 tokenId, TokenLoanParamsWithOracle memory params, address to, bool skim) = abi.decode( datas[i], - (uint256, TokenLoanParams, address, bool) + (uint256, TokenLoanParamsWithOracle, address, bool) ); requestLoan(tokenId, params, to, skim); } else if (action == ACTION_LEND) { - (uint256 tokenId, TokenLoanParams memory params, bool skim) = abi.decode(datas[i], (uint256, TokenLoanParams, bool)); + (uint256 tokenId, TokenLoanParamsWithOracle memory params, bool skim) = abi.decode( + datas[i], + (uint256, TokenLoanParamsWithOracle, bool) + ); lend(tokenId, params, skim); } else if (action == ACTION_BENTO_SETAPPROVAL) { (address user, address _masterContract, bool approved, uint8 v, bytes32 r, bytes32 s) = abi.decode( @@ -719,15 +710,20 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 tokenId, address lender, address recipient, - TokenLoanParams memory params, + TokenLoanParamsWithOracle memory params, bool skimCollateral, bool anyTokenId, SignatureParams memory signature - ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams)); + ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParamsWithOracle, bool, bool, SignatureParams)); requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { - (uint256 tokenId, address borrower, TokenLoanParams memory params, bool skimFunds, SignatureParams memory signature) = abi - .decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); + ( + uint256 tokenId, + address borrower, + TokenLoanParamsWithOracle memory params, + bool skimFunds, + SignatureParams memory signature + ) = abi.decode(datas[i], (uint256, address, TokenLoanParamsWithOracle, bool, SignatureParams)); takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); } } diff --git a/contracts/interfaces/INFTPair.sol b/contracts/interfaces/INFTPair.sol new file mode 100644 index 00000000..c0980a05 --- /dev/null +++ b/contracts/interfaces/INFTPair.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.12 <0.9.0; + +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./IERC721.sol"; + +interface INFTPair { + function collateral() external view returns (IERC721); + + function asset() external view returns (IERC20); + + function masterContract() external view returns (address); + + function bentoBox() external view returns (IBentoBoxV1); + + function removeCollateral(uint256 tokenId, address to) external; +} From b364ed1043d70e72b6fd08f9930c386d10be0977 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 27 May 2022 17:02:08 +0200 Subject: [PATCH 085/107] Remove unused import of BoringRebase --- contracts/NFTPair.sol | 1 - contracts/NFTPairWithOracle.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index af007084..69b4909a 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -23,7 +23,6 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; import "@boringcrypto/boring-solidity/contracts/Domain.sol"; import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; -import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index d4896909..5c702304 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -23,7 +23,6 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringMath.sol"; import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; import "@boringcrypto/boring-solidity/contracts/Domain.sol"; import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; -import "@boringcrypto/boring-solidity/contracts/libraries/BoringRebase.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; From 18186769b5fb9c800b99f4a87ee2f462f95cc2e5 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 27 May 2022 17:02:11 +0200 Subject: [PATCH 086/107] (init and updateLoanParams can be external) --- contracts/NFTPair.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 69b4909a..5f521d3b 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -171,13 +171,13 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } /// @notice De facto constructor for clone contracts - function init(bytes calldata data) public payable override { + function init(bytes calldata data) external payable override { require(address(collateral) == address(0), "NFTPair: already initialized"); (collateral, asset) = abi.decode(data, (IERC721, IERC20)); require(address(collateral) != address(0), "NFTPair: bad pair"); } - function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) public { + function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) external { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { // The lender can change terms so long as the changes are strictly From c05979d2d5119a5a847b9e0f7ff0634cc5c168a5 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Fri, 27 May 2022 17:02:14 +0200 Subject: [PATCH 087/107] Add flash repayments and borrowing - swappers and cook --- contracts/NFTPair.sol | 558 +++++++---- contracts/interfaces/ILendingClub.sol | 20 + contracts/interfaces/INFTBuyer.sol | 18 + contracts/interfaces/INFTPair.sol | 14 + contracts/interfaces/INFTSeller.sol | 17 + contracts/mocks/LendingClubMock.sol | 2 +- contracts/mocks/NFTBuyerSellerMock.sol | 57 ++ contracts/mocks/NFTMarketMock.sol | 66 ++ test/NFTPair.test.ts | 1194 ++++++++++++++++++++---- 9 files changed, 1603 insertions(+), 343 deletions(-) create mode 100644 contracts/interfaces/ILendingClub.sol create mode 100644 contracts/interfaces/INFTBuyer.sol create mode 100644 contracts/interfaces/INFTSeller.sol create mode 100644 contracts/mocks/NFTBuyerSellerMock.sol create mode 100644 contracts/mocks/NFTMarketMock.sol diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 5f521d3b..35723941 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -26,31 +26,10 @@ import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; - -struct TokenLoanParams { - uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 duration; // Length of loan in seconds - uint16 annualInterestBPS; // Variable cost of taking out the loan -} - -interface ILendingClub { - // Per token settings. - function willLend(uint256 tokenId, TokenLoanParams memory params) external view returns (bool); - - function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParams memory); -} - -interface INFTPair { - function collateral() external view returns (IERC721); - - function asset() external view returns (IERC20); - - function masterContract() external view returns (address); - - function bentoBox() external view returns (IBentoBoxV1); - - function removeCollateral(uint256 tokenId, address to) external; -} +import "./interfaces/ILendingClub.sol"; +import "./interfaces/INFTBuyer.sol"; +import "./interfaces/INFTSeller.sol"; +import "./interfaces/INFTPair.sol"; /// @title NFTPair /// @dev This contract allows contract calls to any contract (except BentoBox) @@ -110,7 +89,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; // Highest order term in the Maclaurin series for exp used by - // `calculateIntest`. + // `calculateInterest`. // Intuitive interpretation: interest continuously accrues on the principal. // That interest, in turn, earns "second-order" interest-on-interest, which // itself earns "third-order" interest, etc. This constant determines how @@ -201,42 +180,47 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); } - function _requestLoan( - address collateralProvider, + /// @notice It is the caller's responsibility to ensure skimmed tokens get accounted for somehow so they cannot be used twice. + /// @notice It is the caller's responsibility to ensure `provider` consented to the specific transfer. (EIR-721 approval is not good enough). + function _requireCollateral( + address provider, uint256 tokenId, - TokenLoanParams memory params, - address to, bool skim ) private { - // Edge case: valuation can be zero. That effectively gifts the NFT and - // is therefore a bad idea, but does not break the contract. - require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); if (skim) { require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); } else { - collateral.transferFrom(collateralProvider, address(this), tokenId); + collateral.transferFrom(provider, address(this), tokenId); } - TokenLoan memory loan; - loan.borrower = to; - loan.status = LOAN_REQUESTED; - tokenLoan[tokenId] = loan; - tokenLoanParams[tokenId] = params; - - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); } /// @notice Deposit an NFT as collateral and request a loan against it /// @param tokenId ID of the NFT /// @param to Address to receive the loan, or option to withdraw collateral /// @param params Loan conditions on offer - /// @param skim True if the token has already been transfered + /// @param skim True if the token has already been transferred function requestLoan( uint256 tokenId, TokenLoanParams memory params, address to, bool skim ) public { - _requestLoan(msg.sender, tokenId, params, to, skim); + // Edge case: valuation can be zero. That effectively gifts the NFT and + // is therefore a bad idea, but does not break the contract. + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_INITIAL, "NFTPair: loan exists"); + + loan.borrower = to; + loan.status = LOAN_REQUESTED; + tokenLoan[tokenId] = loan; + tokenLoanParams[tokenId] = params; + + emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + _requireCollateral(msg.sender, tokenId, skim); } /// @notice Removes `tokenId` as collateral and transfers it to `to`. @@ -266,27 +250,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { emit LogRemoveCollateral(tokenId, to); } - // Assumes the lender has agreed to the loan. + ///@notice Assumes the lender has agreed to the loan. + ///@param borrower Receives the option to repay and get the collateral back + ///@param initialRecipient Receives the initial funds function _lend( address lender, + address borrower, + address initialRecipient, uint256 tokenId, - TokenLoanParams memory accepted, + uint256 amount, bool skim - ) internal { - TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); - TokenLoanParams memory params = tokenLoanParams[tokenId]; - - // Valuation has to be an exact match, everything else must be at least - // as good for the lender as `accepted`. - require( - params.valuation == accepted.valuation && - params.duration <= accepted.duration && - params.annualInterestBPS >= accepted.annualInterestBPS, - "NFTPair: bad params" - ); - - uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); + ) internal returns (uint256 borrowerShare) { + uint256 totalShare = bentoBox.toShare(asset, amount, false); // No overflow: at most 128 + 16 bits (fits in BentoBox) uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; @@ -300,12 +275,14 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); } // No underflow: follows from OPEN_FEE_BPS <= BPS - uint256 borrowerShare = totalShare - openFeeShare; - bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); + borrowerShare = totalShare - openFeeShare; + bentoBox.transfer(asset, address(this), initialRecipient, borrowerShare); // No overflow: addends (and result) must fit in BentoBox feesEarnedShare += protocolFeeShare; + TokenLoan memory loan; loan.lender = lender; + loan.borrower = borrower; loan.status = LOAN_OUTSTANDING; loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; @@ -316,13 +293,25 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @notice Lends with the parameters specified by the borrower. /// @param tokenId ID of the token that will function as collateral /// @param accepted Loan parameters as the lender saw them, for security - /// @param skim True if the funds have been transfered to the contract + /// @param skim True if the funds have been transferred to the contract function lend( uint256 tokenId, TokenLoanParams memory accepted, bool skim ) public { - _lend(msg.sender, tokenId, accepted, skim); + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); + TokenLoanParams memory requested = tokenLoanParams[tokenId]; + + // Valuation has to be an exact match, everything else must be at least + // as good for the lender as `accepted`. + require( + requested.valuation == accepted.valuation && + requested.duration <= accepted.duration && + requested.annualInterestBPS >= accepted.annualInterestBPS, + "NFTPair: bad params" + ); + _lend(msg.sender, loan.borrower, loan.borrower, tokenId, requested.valuation, skim); } // solhint-disable-next-line func-name-mixedcase @@ -345,26 +334,94 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @notice Caller provides collateral; loan can go to a different address. /// @param tokenId ID of the token that will function as collateral /// @param lender Lender, whose BentoBox balance the funds will come from - /// @param recipient Address to receive the loan. + /// @param borrower Receives the funds and the option to repay /// @param params Loan parameters requested, and signed by the lender - /// @param skimCollateral True if the collateral has already been transfered + /// @param skimCollateral True if the collateral has already been transferred /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. function requestAndBorrow( uint256 tokenId, address lender, - address recipient, + address borrower, TokenLoanParams memory params, bool skimCollateral, bool anyTokenId, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s + SignatureParams memory signature ) public { - if (v == 0 && r == bytes32(0) && s == bytes32(0)) { + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + _lend(lender, borrower, borrower, tokenId, params.valuation, false); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + // (The check is in `_requireSignedLendParams()`) + _requireCollateral(msg.sender, tokenId, skimCollateral); + } + + ///@param borrower Also receives excess if token cheaper than loan amount + function flashRequestAndBorrow( + uint256 tokenId, + address lender, + address borrower, + TokenLoanParams memory params, + bool anyTokenId, + SignatureParams memory signature, + uint256 price, + INFTBuyer buyer, + bool skimShortage + ) external { + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + // Round up: this is how many Bento-shares it will take to withdraw + // `price` tokens + uint256 priceShare = bentoBox.toShare(asset, price, true); + // Bento-shares received by taking out the loan. They are sent to the + // buyer contract for skimming. + // TODO: Allow Bento-withdrawing instead? + uint256 borrowerShare = _lend(lender, borrower, address(this), tokenId, params.valuation, false); + // At this point the contract has `borrowerShare` extra shares. If this + // is too much, then the borrower gets the excess. If this is not + // enough, we either take the rest from msg.sender, or have the amount + // skimmed. + if (borrowerShare > priceShare) { + bentoBox.transfer(asset, address(this), borrower, borrowerShare - priceShare); + } else if (borrowerShare < priceShare) { + if (skimShortage) { + // We have `borrowerShare`, but need `priceShare`: + require(bentoBox.balanceOf(asset, address(this)) >= (priceShare + feesEarnedShare), "NFTPair: skim too much"); + } else { + // We need the difference: + bentoBox.transfer(asset, msg.sender, address(this), priceShare - borrowerShare); + } + } + // The share amount taken will be exactly `priceShare`, and the token + // amount will be exactly `price`. If we passed `priceShare` instead, + // the token amount given could be different. + bentoBox.withdraw(asset, address(this), address(buyer), price, 0); + + // External call is safe: At this point, the state of the contract is + // unusual only in that it has issued a loan against the token that has + // not been delivered yet. Any interaction that does not involve this + // loan/token is no different from outside this call. Taking out a new + // loan is not possible. Repaying the loan is, but requires that: + // a) the buyer either is, or has the token sent to, the borrower; + // b) the token is sent to the contract first -- `_repayBefore()` will + // try to transfer it away. + // By (b) in particular, the buyer contract cannot exploit this + // situation. + buyer.buy(asset, price, collateral, tokenId, address(this)); + require(collateral.ownerOf(tokenId) == address(this), "NFTPair: buyer failed"); + } + + function _requireSignedLendParams( + address lender, + uint256 tokenId, + TokenLoanParams memory params, + bool anyTokenId, + SignatureParams memory signature + ) private { + if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); } else { - require(block.timestamp <= deadline, "NFTPair: signature expired"); + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; bytes32 dataHash = keccak256( abi.encode( @@ -376,32 +433,23 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { params.duration, params.annualInterestBPS, nonce, - deadline + signature.deadline ) ); - require(ecrecover(_getDigest(dataHash), v, r, s) == lender, "NFTPair: signature invalid"); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == lender, "NFTPair: signature invalid"); } - _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); - _lend(lender, tokenId, params, false); + + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + tokenLoanParams[tokenId] = params; } - /// @notice Take collateral from a pre-commited borrower and lend against it - /// @notice Collateral must come from the borrower, not a third party. - /// @param tokenId ID of the token that will function as collateral - /// @param borrower Address that provides collateral and receives the loan - /// @param params Loan terms offered, and signed by the borrower - /// @param skimFunds True if the funds have been transfered to the contract - function takeCollateralAndLend( - uint256 tokenId, + function _requireSignedBorrowParams( address borrower, + uint256 tokenId, TokenLoanParams memory params, - bool skimFunds, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) public { - require(block.timestamp <= deadline, "NFTPair: signature expired"); + SignatureParams memory signature + ) private { + require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[borrower]++; bytes32 dataHash = keccak256( abi.encode( @@ -412,12 +460,37 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { params.duration, params.annualInterestBPS, nonce, - deadline + signature.deadline ) ); - require(ecrecover(_getDigest(dataHash), v, r, s) == borrower, "NFTPair: signature invalid"); - _requestLoan(borrower, tokenId, params, borrower, false); - _lend(msg.sender, tokenId, params, skimFunds); + require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == borrower, "NFTPair: signature invalid"); + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + tokenLoanParams[tokenId] = params; + } + + /// @notice Take collateral from a pre-commited borrower and lend against it + /// @notice Collateral must come from the borrower, not a third party. + /// @param tokenId ID of the token that will function as collateral + /// @param borrower Address that provides collateral and receives the loan + /// @param params Loan terms offered, and signed by the borrower + /// @param skimFunds True if the funds have been transfered to the contract + function takeCollateralAndLend( + uint256 tokenId, + address borrower, + TokenLoanParams memory params, + bool skimFunds, + SignatureParams memory signature + ) public { + _requireSignedBorrowParams(borrower, tokenId, params, signature); + _lend(msg.sender, borrower, borrower, tokenId, params.valuation, skimFunds); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + // (The check is in `_requireSignedBorrowParams()`) + // Taking collateral from someone other than msg.sender is safe: the + // borrower signed a message giving permission. + _requireCollateral(borrower, tokenId, false); } /// Approximates continuous compounding. Uses Horner's method to evaluate @@ -454,7 +527,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // // which approaches, but never exceeds the "theoretical" result, // - // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 + // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 ] // // as n goes to infinity. We use the fact that // @@ -501,9 +574,19 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } } - function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { + function _repayBefore(uint256 tokenId, address to) + public + returns ( + uint256 totalShare, + uint256 totalAmount, + uint256 feeShare, + address lender + ) + { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); + require(msg.sender == loan.borrower || to == loan.borrower, "NFTPair: not borrower"); + TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; require( // Addition is safe: both summands are smaller than 256 bits @@ -515,33 +598,119 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // No underflow: loan.startTime is only ever set to a block timestamp // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); + // No overflow: multiplicands fit in 128 and 16 bits uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; - amount = principal + interest; + // No overflon: both terms are 128 bits + totalAmount = principal + interest; - uint256 totalShare = bentoBox.toShare(asset, amount, false); - uint256 feeShare = bentoBox.toShare(asset, fee, false); + totalShare = bentoBox.toShare(asset, totalAmount, false); + feeShare = bentoBox.toShare(asset, fee, false); + lender = loan.lender; + + delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; - address from; + collateral.transferFrom(address(this), to, tokenId); + } + + function _repayAfter( + address lender, + uint256 totalShare, + uint256 feeShare, + uint256 tokenId, + bool skim + ) private { + // No overflow: `totalShare - feeShare` is 90% of `totalShare`, and + // if that exceeds 128 bits the BentoBox transfer will revert. It + // follows that `totalShare` fits in 129 bits, and `feesEarnedShare` + // fits in 128 as it represents a BentoBox balance. + // Skimming is safe: the amount gets transfered to the lender later, + // and therefore cannot be skimmed twice. if (skim) { require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); - from = address(this); - // No overflow: result fits in BentoBox } else { - bentoBox.transfer(asset, msg.sender, address(this), feeShare); - from = msg.sender; + bentoBox.transfer(asset, msg.sender, address(this), totalShare); } - // No underflow: PROTOCOL_FEE_BPS < BPS by construction. + // No overflow: result fits in BentoBox feesEarnedShare += feeShare; - delete tokenLoan[tokenId]; + // No underflow: `feeShare` is 10% of part of `totalShare` + bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); + emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } - bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); - collateral.transferFrom(address(this), loan.borrower, tokenId); + function repay( + uint256 tokenId, + address to, + bool skim + ) external { + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to); + _repayAfter(lender, totalShare, feeShare, tokenId, skim); + } - emit LogRepay(from, tokenId); + function flashRepay( + uint256 tokenId, + uint256 price, + INFTSeller seller, + address excessRecipient, + bool skimShortage + ) external { + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller)); + + // External call is safe: At this point the loan is already gone, the + // seller has the token, and an amount must be paid via skimming or the + // entire transaction reverts. + // Other than being owed the money, the contract is in a valid state, + // and once payment is received it is "accounted for" by being sent + // away (in `_repayAfter()`), so that it cannot be reused for skimming. + // Relying on return value is safe: if the amount reported is too high, + // then either `_repayAfter()` will fail, or the funds were sitting in + // the contract's BentoBox balance unaccounted for, and could be freely + // skimmed for another purpose anyway. + uint256 priceShare = seller.sell(collateral, tokenId, asset, price, address(this)); + if (priceShare < totalShare) { + // No overflow: `totalShare` fits or `_repayAfter()` reverts. See + // comments there for proof. + // If we are skimming, then we defer the check to `_repayAfter()`, + // which checks that the full amount (`totalShare`) has been sent. + if (!skimShortage) { + bentoBox.transfer(asset, msg.sender, address(this), totalShare - priceShare); + } + } else if (priceShare > totalShare) { + bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); + } + _repayAfter(lender, totalShare, feeShare, tokenId, true); + } + + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + address to = masterContract.feeTo(); + + uint256 _share = feesEarnedShare; + if (_share > 0) { + bentoBox.transfer(asset, address(this), to, _share); + feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, _share); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); } - uint8 internal constant ACTION_REPAY = 2; + //// Cook actions + + // Information only + uint8 internal constant ACTION_GET_AMOUNT_DUE = 1; + uint8 internal constant ACTION_GET_SHARES_DUE = 2; + + // End up owing collateral + uint8 internal constant ACTION_REPAY = 3; uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; uint8 internal constant ACTION_REQUEST_LOAN = 12; @@ -625,23 +794,54 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { return (returnData, returnValues); } - /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. - /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). - /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. - /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. - /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. - /// @return value1 May contain the first positioned return value of the last executed action (if applicable). - /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). - function cook( + // (For the cook action) + function _getAmountDue(uint256 tokenId) private view returns (uint256) { + TokenLoanParams memory params = tokenLoanParams[tokenId]; + // No underflow: startTime is always set to some block timestamp + uint256 principal = params.valuation; + uint256 interest = calculateInterest(principal, uint64(block.timestamp - tokenLoan[tokenId].startTime), params.annualInterestBPS); + // No overflow: both terms are 128 bits + return principal + interest; + } + + function _cook( uint8[] calldata actions, uint256[] calldata values, - bytes[] calldata datas - ) external payable returns (uint256 value1, uint256 value2) { - for (uint256 i = 0; i < actions.length; i++) { + bytes[] calldata datas, + uint256 i, + uint256[2] memory result + ) private { + for (; i < actions.length; i++) { uint8 action = actions[i]; - if (action == ACTION_REPAY) { - (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); - repay(tokenId, skim); + if (action == ACTION_GET_AMOUNT_DUE) { + uint256 tokenId = abi.decode(datas[i], (uint256)); + result[0] = _getAmountDue(tokenId); + } else if (action == ACTION_GET_SHARES_DUE) { + uint256 tokenId = abi.decode(datas[i], (uint256)); + result[1] = _getAmountDue(tokenId); + result[0] = bentoBox.toShare(asset, result[1], false); + } else if (action == ACTION_REPAY) { + uint256 tokenId; + uint256 totalShare; + uint256 feeShare; + address lender; + bool skim; + { + address to; + // No skimming, but it can sill be done + (tokenId, to, skim) = abi.decode(datas[i], (uint256, address, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to); + // Delaying asset collection until after the rest of the + // cook is safe: after checking.. - `feesEarnedShare` is + // updated after the check - The rest (`totalShare - + // feeShare`) is transferred away It is therefore not + // possible to skim the same amount twice. + // (Reusing `i` slot for stack depth reasons) + } + result[0] = totalShare; + _cook(actions, values, datas, ++i, result); + _repayAfter(lender, totalShare, feeShare, tokenId, skim); + return; } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); @@ -661,71 +861,77 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { ); bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); } else if (action == ACTION_BENTO_DEPOSIT) { - (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); + (result[0], result[1]) = _bentoDeposit(datas[i], values[i], result[0], result[1]); } else if (action == ACTION_BENTO_WITHDRAW) { - (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + (result[0], result[1]) = _bentoWithdraw(datas[i], result[0], result[1]); } else if (action == ACTION_BENTO_TRANSFER) { (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); - bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); + bentoBox.transfer(token, msg.sender, to, _num(share, result[0], result[1])); } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); bentoBox.transferMultiple(token, msg.sender, tos, shares); } else if (action == ACTION_CALL) { - (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], result[0], result[1]); if (returnValues == 1) { - (value1) = abi.decode(returnData, (uint256)); + (result[0]) = abi.decode(returnData, (uint256)); } else if (returnValues == 2) { - (value1, value2) = abi.decode(returnData, (uint256, uint256)); + (result[0], result[1]) = abi.decode(returnData, (uint256, uint256)); } } else if (action == ACTION_REQUEST_AND_BORROW) { - ( - uint256 tokenId, - address lender, - address recipient, - TokenLoanParams memory params, - bool skimCollateral, - bool anyTokenId, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParams, bool, bool, uint256, uint8, bytes32, bytes32)); - requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, deadline, v, r, s); + bool skimCollateral; + uint256 tokenId; + { + address lender; + address borrower; + TokenLoanParams memory params; + bool anyTokenId; + SignatureParams memory signature; + (tokenId, lender, borrower, params, skimCollateral, anyTokenId, signature) = abi.decode( + datas[i], + (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams) + ); + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + _lend(lender, borrower, borrower, tokenId, params.valuation, false); + } + _cook(actions, values, datas, ++i, result); + // Skimming is safe: + // - This call both requires loan state to be LOAN_INITIAL and + // sets it to something else. Every other use of + // `_requireCollateral()` must uphold that same requirement; + // see to it. + // Delaying until after the rest of the cook is safe: + // - If the rest of the cook _also_ takes this collateral + // somehow -- either via skimming, or via just having it + // transfered in -- then it did so by opening a loan. But + // that is only possible if this one (that we are collecting + // the collateral for) got repaid in the mean time, which is + // a silly thing to do, but otherwise legitimate and not an + // exploit. + _requireCollateral(msg.sender, tokenId, skimCollateral); + return; } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { - ( - uint256 tokenId, - address borrower, - TokenLoanParams memory params, - bool skimFunds, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) = abi.decode(datas[i], (uint256, address, TokenLoanParams, bool, uint256, uint8, bytes32, bytes32)); - takeCollateralAndLend(tokenId, borrower, params, skimFunds, deadline, v, r, s); + (uint256 tokenId, address borrower, TokenLoanParams memory params, bool skimFunds, SignatureParams memory signature) = abi + .decode(datas[i], (uint256, address, TokenLoanParams, bool, SignatureParams)); + takeCollateralAndLend(tokenId, borrower, params, skimFunds, signature); } } } - /// @notice Withdraws the fees accumulated. - function withdrawFees() public { - address to = masterContract.feeTo(); - - uint256 _share = feesEarnedShare; - if (_share > 0) { - bentoBox.transfer(asset, address(this), to, _share); - feesEarnedShare = 0; - } - - emit LogWithdrawFees(to, _share); - } - - /// @notice Sets the beneficiary of fees accrued in liquidations. - /// MasterContract Only Admin function. - /// @param newFeeTo The address of the receiver. - function setFeeTo(address newFeeTo) public onlyOwner { - feeTo = newFeeTo; - emit LogFeeTo(newFeeTo); + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + uint256[2] memory result; + _cook(actions, values, datas, 0, result); + return (result[0], result[1]); } } diff --git a/contracts/interfaces/ILendingClub.sol b/contracts/interfaces/ILendingClub.sol new file mode 100644 index 00000000..b1a0b936 --- /dev/null +++ b/contracts/interfaces/ILendingClub.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.12 <0.9.0; +pragma experimental ABIEncoderV2; + +import "./INFTPair.sol"; + +interface ILendingClub { + // Per token settings. + function willLend(uint256 tokenId, TokenLoanParams memory params) + external + view + returns (bool); + + function lendingConditions(address nftPair, uint256 tokenId) + external + view + returns (TokenLoanParams memory); +} + diff --git a/contracts/interfaces/INFTBuyer.sol b/contracts/interfaces/INFTBuyer.sol new file mode 100644 index 00000000..37a091c9 --- /dev/null +++ b/contracts/interfaces/INFTBuyer.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./IERC721.sol"; + +interface INFTBuyer { + // Must be ERC20-skimming (not Bento). + // Must revert on failure. + // Transfers the NFT to `recipient`. + function buy( + IERC20 fromAsset, + uint256 fromAmount, + IERC721 toContract, + uint256 toTokenId, + address recipient + ) external; +} diff --git a/contracts/interfaces/INFTPair.sol b/contracts/interfaces/INFTPair.sol index c0980a05..ff26e75e 100644 --- a/contracts/interfaces/INFTPair.sol +++ b/contracts/interfaces/INFTPair.sol @@ -17,3 +17,17 @@ interface INFTPair { function removeCollateral(uint256 tokenId, address to) external; } + +struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 annualInterestBPS; // Variable cost of taking out the loan +} + +struct SignatureParams { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + diff --git a/contracts/interfaces/INFTSeller.sol b/contracts/interfaces/INFTSeller.sol new file mode 100644 index 00000000..fd86339b --- /dev/null +++ b/contracts/interfaces/INFTSeller.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +import "@boringcrypto/boring-solidity/contracts/interfaces/IERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./IERC721.sol"; + +interface INFTSeller { + // Must be ERC721-skimming. Proceeds go to `recipient`'s BentoBox account + // Must revert on failure. + function sell( + IERC721 fromContract, + uint256 fromTokenId, + IERC20 toAsset, + uint256 toAmount, + address recipient + ) external returns (uint256 toShares); +} diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index dfa5d872..0965e5ee 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -3,7 +3,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; -import "../NFTPair.sol"; +import "../interfaces/INFTPair.sol"; // Minimal implementation to set up some tests. contract LendingClubMock { diff --git a/contracts/mocks/NFTBuyerSellerMock.sol b/contracts/mocks/NFTBuyerSellerMock.sol new file mode 100644 index 00000000..222cf1af --- /dev/null +++ b/contracts/mocks/NFTBuyerSellerMock.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./NFTMarketMock.sol"; +import "../interfaces/IERC721.sol"; +import "../interfaces/INFTBuyer.sol"; +import "../interfaces/INFTSeller.sol"; + +// NFT version of swappers. We could of course make our mock "market" conform +// to this pattern, but this serves as an illustration of how an abitrary +// external contract might be used. +contract NFTBuyerSellerMock is INFTBuyer, INFTSeller { + using BoringERC20 for IERC20; + + IBentoBoxV1 private immutable bentoBox; + NFTMarketMock private immutable market; + + constructor(IBentoBoxV1 _bentoBox, NFTMarketMock _market) public { + bentoBox = _bentoBox; + market = _market; + } + + function buy( + IERC20 fromAsset, + uint256 fromAmount, + IERC721 toContract, + uint256 toTokenId, + address recipient + ) external override { + require(fromAsset == market.money(), "Buyer: wrong token"); + require(toContract == market.nfts(), "Buyer: wrong contract"); + fromAsset.safeTransfer(address(market), fromAmount); + market.buy(toTokenId, fromAmount, recipient, true); + } + + function sell( + IERC721 fromContract, + uint256 fromTokenId, + IERC20 toAsset, + uint256 toAmount, + address recipient + ) external override returns (uint256 toShares) { + // (Ignore these and/or make them specific to the contract?) + require(fromContract == market.nfts(), "Seller: wrong contract"); + require(toAsset == market.money(), "Seller: wrong token"); + // We assume we have been transfered the NFT. We also use skimming to + // interact with the market contract, to avoid having to approve it: + fromContract.transferFrom(address(this), address(market), fromTokenId); + // We will use (Bento-)skimming to send the funds back to the + // recipient's BentoBox balance: + market.sell(fromTokenId, toAmount, address(bentoBox), true); + (, toShares) = bentoBox.deposit(toAsset, address(bentoBox), recipient, toAmount, 0); + } +} diff --git a/contracts/mocks/NFTMarketMock.sol b/contracts/mocks/NFTMarketMock.sol new file mode 100644 index 00000000..ad63d56c --- /dev/null +++ b/contracts/mocks/NFTMarketMock.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "../interfaces/IERC721.sol"; +import "../interfaces/INFTPair.sol"; + +contract NFTMarketMock { + using BoringERC20 for IERC20; + + IERC721 public immutable nfts; + IERC20 public immutable money; + uint256 public reserves; + mapping(uint256 => bool) public inventory; + + constructor(IERC721 _nfts, IERC20 _money) public { + money = _money; + nfts = _nfts; + } + + function fund(uint256 amount) external { + money.safeTransferFrom(msg.sender, address(this), amount); + reserves += amount; + } + + function stock(uint256 tokenId) external { + inventory[tokenId] = true; + nfts.transferFrom(msg.sender, address(this), tokenId); + } + + function sell( + uint256 tokenId, + uint256 price, + address to, + bool skim + ) external { + require(price <= reserves, "expensive"); + if (skim) { + require(inventory[tokenId] == false, "scam"); + require(nfts.ownerOf(tokenId) == address(this), "skim"); + } else { + nfts.transferFrom(msg.sender, address(this), tokenId); + } + reserves -= price; + inventory[tokenId] = true; + money.safeTransfer(to, price); + } + + function buy( + uint256 tokenId, + uint256 price, + address to, + bool skim + ) external { + require(inventory[tokenId] == true, "n/a"); + if (skim) { + require(money.balanceOf(address(this)) >= price + reserves, "skim"); + } else { + money.safeTransferFrom(msg.sender, address(this), price); + } + reserves += price; + inventory[tokenId] = false; + nfts.transferFrom(address(this), to, tokenId); + } +} diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 9a09ef03..25a4e019 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -10,8 +10,10 @@ const MaxUint128 = BigNumber.from(2).pow(128).sub(1); const hashUtf8String = (s: string) => keccak256(toUtf8Bytes(s)); +const zeroSign = (deadline) => ({ r: HashZero, s: HashZero, v: 0, deadline }); + import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; -import { BentoBoxMock, ERC20Mock, ERC721Mock, LendingClubMock, WETH9Mock, NFTPair } from "../typechain"; +import { BentoBoxMock, ERC20Mock, ERC721Mock, LendingClubMock, NFTMarketMock, NFTBuyerSellerMock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; const LoanStatus = { @@ -21,7 +23,9 @@ const LoanStatus = { }; // Cook actions -const ACTION_REPAY = 2; +const ACTION_GET_AMOUNT_DUE = 1; +const ACTION_GET_SHARES_DUE = 2; +const ACTION_REPAY = 3; const ACTION_REMOVE_COLLATERAL = 4; const ACTION_REQUEST_LOAN = 12; @@ -64,6 +68,13 @@ interface IPartialLoanParams { annualInterestBPS?: number; } +interface ISignature { + r: string; + s: string; + v: number; + deadline: number; +} + const DOMAIN_SEPARATOR_HASH = hashUtf8String("EIP712Domain(uint256 chainId,address verifyingContract)"); const DAY = 24 * 3600; @@ -72,6 +83,7 @@ const nextYear = Math.floor(new Date().getTime() / 1000) + YEAR; const nextDecade = Math.floor(new Date().getTime() / 1000) + YEAR * 10; describe("NFT Pair", async () => { + let chainId: BigNumberish; let apes: ERC721Mock; let guineas: ERC20Mock; let weth: WETH9Mock; @@ -81,6 +93,7 @@ describe("NFT Pair", async () => { let alice: SignerWithAddress; let bob: SignerWithAddress; let carol: SignerWithAddress; + let apesMarket: NFTMarketMock; // Named token IDs for testing.. let apeIds: { @@ -121,7 +134,6 @@ describe("NFT Pair", async () => { }); // Specific to the mock implementation.. - // TODO: Upgrade BoringSolidity to version that returns the ID: const mintToken = async (mockContract, ownerAddress) => { const id = await mockContract.totalSupply(); await mockContract.mint(ownerAddress); @@ -129,7 +141,89 @@ describe("NFT Pair", async () => { }; const mintApe = (ownerAddress) => mintToken(apes, ownerAddress); + const signLendRequest = async (pair, wallet, { tokenId, anyTokenId, valuation, duration, annualInterestBPS, deadline }) => { + const sigTypes = [ + { name: "contract", type: "address" }, + { name: "tokenId", type: "uint256" }, + { name: "anyTokenId", type: "bool" }, + { name: "valuation", type: "uint128" }, + { name: "duration", type: "uint64" }, + { name: "annualInterestBPS", type: "uint16" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ]; + const sigValues = { + contract: pair.address, + tokenId, + anyTokenId, + valuation, + duration, + annualInterestBPS, + nonce: 0, + deadline, + }; + const sig = await wallet._signTypedData( + // The stuff going into DOMAIN_SEPARATOR: + { chainId, verifyingContract: masterContract.address }, + + // sigHash + { Lend: sigTypes }, + sigValues + ); + return { deadline, ...splitSignature(sig) }; + }; + + const signBorrowRequest = async (pair, wallet, { tokenId, valuation, duration, annualInterestBPS, deadline }) => { + const sigTypes = [ + { name: "contract", type: "address" }, + { name: "tokenId", type: "uint256" }, + { name: "valuation", type: "uint128" }, + { name: "duration", type: "uint64" }, + { name: "annualInterestBPS", type: "uint16" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ]; + // const sigArgs = sigTypes.map((t) => t.type + " " + t.name); + // const sigHash = keccak256( + // toUtf8Bytes("Borrow(" + sigArgs.join(",") + ")") + // ); + + const sigValues = { + contract: pair.address, + tokenId, + valuation, + duration, + annualInterestBPS, + nonce: 0, + deadline, + }; + // const dataHash = keccak256(defaultAbiCoder.encode( + // ["bytes32 sigHash", ...sigArgs], + // Object.values({ sigHash, ...sigValues }) + // )); + // const digest = keccak256( + // solidityPack( + // ["string", "bytes32", "bytes32"], + // ["\x19\x01", DOMAIN_SEPARATOR, dataHash] + // ) + // ); + + // At this point we'd like to sign this digest, but signing arbitrary + // data is made difficult in ethers.js to prevent abuse. So for now we + // use a helper method that basically does everything we just did again: + const sig = await wallet._signTypedData( + // The stuff going into DOMAIN_SEPARATOR: + { chainId, verifyingContract: masterContract.address }, + + // sigHash + { Borrow: sigTypes }, + sigValues + ); + return { deadline, ...splitSignature(sig) }; + }; + before(async () => { + chainId = (await ethers.provider.getNetwork()).chainId; weth = await deployContract("WETH9Mock"); // The BentoBox complains if totalSupply = 0, and total supply is however // many ETH has been deposited: @@ -141,6 +235,8 @@ describe("NFT Pair", async () => { apes = await deployContract("ERC721Mock"); guineas = await deployContract("ERC20Mock", MaxUint256); + apesMarket = await deployContract("NFTMarketMock", apes.address, guineas.address); + const addresses = await getNamedAccounts(); deployer = await ethers.getSigner(addresses.deployer); alice = await ethers.getSigner(addresses.alice); @@ -644,7 +740,7 @@ describe("NFT Pair", async () => { // Two Bento transfers: payment to the lender, fee to the contract await advanceNextTime(DAY); - await expect(pair.connect(alice).repay(apeIds.aliceOne, false)) + await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -686,7 +782,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await advanceNextTime(DAY); - await expect(pair.connect(carol).repay(apeIds.aliceOne, false)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(carol.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -737,7 +833,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, true)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, true)) .to.emit(pair, "LogRepay") .withArgs(pair.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -773,6 +869,125 @@ describe("NFT Pair", async () => { expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); }); + // Simple scenario to help refactor `cook()`: + it("Should allow paying off loans for someone else (cook)", async () => { + // ..and take from the correct person: + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + carol: await bentoBox.balanceOf(guineas.address, carol.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + await advanceNextTime(DAY); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, alice.address, false])); + + await expect(pair.connect(carol).cook(actions, values, datas)) + .to.emit(pair, "LogRepay") + .withArgs(carol.address, apeIds.aliceOne) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(DAY, params); + + // Alice paid or received nothing: + expect(t0.alice).to.equal(t1.alice); + + const paid = t0.carol.sub(t1.carol); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); + }); + + it("Should allow paying off loans for someone else (c+s)", async () => { + const interval = 234 * DAY + 5678; + // Does not matter who supplies the payment. Note that there will be + // an excess left; skimming is really only suitable for contracts that + // can calculate the exact repayment needed: + const exactAmount = params.valuation.add(await pair.calculateInterest(params.valuation, interval, params.annualInterestBPS)); + // The contract rounds down; we round up and add a little: + const closeToShare = exactAmount.mul(9).add(19).div(20); + const enoughShare = closeToShare.add(getBigNumber(1337, 8)); + + // This would normally be done in the same transaction... + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + carol: await bentoBox.balanceOf(guineas.address, carol.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + // Calculate repay share exactly + actions.push(ACTION_GET_SHARES_DUE); + values.push(0); + datas.push(encodeParameters(["uint256"], [apeIds.aliceOne])); + + actions.push(ACTION_BENTO_TRANSFER); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256"], [guineas.address, pair.address, USE_VALUE1])); + + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, alice.address, true])); + + await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); + await expect(pair.connect(carol).cook(actions, values, datas)) + .to.emit(pair, "LogRepay") + .withArgs(pair.address, apeIds.aliceOne) + .to.emit(apes, "Transfer") + .withArgs(pair.address, alice.address, apeIds.aliceOne) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(interval, params); + + // We essentially did a normal repayment, so expect the same balance + // changes as in the case where we are not skimming: + + // Alice paid or received nothing: + expect(t0.alice).to.equal(t1.alice); + + const paid = t0.carol.sub(t1.carol); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.lte(paid.sub(valuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); + }); + it("Should work for a large, but repayable, number", async () => { const fiveYears = 5 * YEAR; const large: ILoanParams = { @@ -804,7 +1019,7 @@ describe("NFT Pair", async () => { const inFive = await advanceNextTime(fiveYears); - await expect(pair.connect(alice).repay(apeIds.aliceTwo, false)) + await expect(pair.connect(alice).repay(apeIds.aliceTwo, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceTwo) .to.emit(apes, "Transfer") @@ -842,12 +1057,12 @@ describe("NFT Pair", async () => { it("Should refuse repayments on expired loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); - await expect(pair.connect(alice).repay(apeIds.aliceOne, false)).to.be.revertedWith("NFTPair: loan expired"); + await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, false)).to.be.revertedWith("NFTPair: loan expired"); }); it("Should refuse repayments on nonexistent loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); - await expect(pair.connect(carol).repay(apeIds.carolOne, false)).to.be.revertedWith("NFTPair: no loan"); + await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, false)).to.be.revertedWith("NFTPair: no loan"); }); it("Should refuse to skim too much", async () => { @@ -862,13 +1077,12 @@ describe("NFT Pair", async () => { await bentoBox.connect(bob).transfer(guineas.address, bob.address, pair.address, notEnoughShare); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, true)).to.be.revertedWith("NFTPair: skim too much"); + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, true)).to.be.revertedWith("NFTPair: skim too much"); }); }); describeSnapshot("Signed Lend/Borrow", () => { let pair: NFTPair; - let chainId: BigNumberish; let DOMAIN_SEPARATOR: string; let BORROW_SIGNATURE_HASH: string; let LEND_SIGNATURE_HASH: string; @@ -876,8 +1090,6 @@ describe("NFT Pair", async () => { before(async () => { pair = await deployPair(); - chainId = (await ethers.provider.getNetwork()).chainId; - DOMAIN_SEPARATOR = keccak256( defaultAbiCoder.encode(["bytes32", "uint256", "address"], [DOMAIN_SEPARATOR_HASH, chainId, masterContract.address]) ); @@ -887,87 +1099,6 @@ describe("NFT Pair", async () => { } }); - const signBorrowRequest = async (wallet, { tokenId, valuation, duration, annualInterestBPS, deadline }) => { - const sigTypes = [ - { name: "contract", type: "address" }, - { name: "tokenId", type: "uint256" }, - { name: "valuation", type: "uint128" }, - { name: "duration", type: "uint64" }, - { name: "annualInterestBPS", type: "uint16" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ]; - // const sigArgs = sigTypes.map((t) => t.type + " " + t.name); - // const sigHash = keccak256( - // toUtf8Bytes("Borrow(" + sigArgs.join(",") + ")") - // ); - - const sigValues = { - contract: pair.address, - tokenId, - valuation, - duration, - annualInterestBPS, - nonce: 0, - deadline, - }; - // const dataHash = keccak256(defaultAbiCoder.encode( - // ["bytes32 sigHash", ...sigArgs], - // Object.values({ sigHash, ...sigValues }) - // )); - // const digest = keccak256( - // solidityPack( - // ["string", "bytes32", "bytes32"], - // ["\x19\x01", DOMAIN_SEPARATOR, dataHash] - // ) - // ); - - // At this point we'd like to sign this digest, but signing arbitrary - // data is made difficult in ethers.js to prevent abuse. So for now we - // use a helper method that basically does everything we just did again: - const sig = await wallet._signTypedData( - // The stuff going into DOMAIN_SEPARATOR: - { chainId, verifyingContract: masterContract.address }, - - // sigHash - { Borrow: sigTypes }, - sigValues - ); - return splitSignature(sig); - }; - - const signLendRequest = async (wallet, { tokenId, anyTokenId, valuation, duration, annualInterestBPS, deadline }) => { - const sigTypes = [ - { name: "contract", type: "address" }, - { name: "tokenId", type: "uint256" }, - { name: "anyTokenId", type: "bool" }, - { name: "valuation", type: "uint128" }, - { name: "duration", type: "uint64" }, - { name: "annualInterestBPS", type: "uint16" }, - { name: "nonce", type: "uint256" }, - { name: "deadline", type: "uint256" }, - ]; - const sigValues = { - contract: pair.address, - tokenId, - anyTokenId, - valuation, - duration, - annualInterestBPS, - nonce: 0, - deadline, - }; - const sig = await wallet._signTypedData( - // The stuff going into DOMAIN_SEPARATOR: - { chainId, verifyingContract: masterContract.address }, - - // sigHash - { Lend: sigTypes }, - sigValues - ); - return splitSignature(sig); - }; - it("Should have the expected DOMAIN_SEPARATOR", async () => { expect(DOMAIN_SEPARATOR).to.equal(await pair.DOMAIN_SEPARATOR()); }); @@ -985,7 +1116,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { v, r, s } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: apeIds.carolOne, anyTokenId: false, valuation, @@ -998,21 +1129,8 @@ describe("NFT Pair", async () => { await expect( pair .connect(carol) - .requestAndBorrow( - apeIds.carolOne, - bob.address, - carol.address, - { valuation, duration, annualInterestBPS }, - false, - false, - deadline, - v, - r, - s - ) - ) - .to.emit(pair, "LogRequestLoan") - .to.emit(pair, "LogLend"); + .requestAndBorrow(apeIds.carolOne, bob.address, carol.address, { valuation, duration, annualInterestBPS }, false, false, sigParams) + ).to.emit(pair, "LogLend"); }); it("Should support pre-approving a loan request for any token", async () => { @@ -1025,7 +1143,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { v, r, s } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: 0, anyTokenId: true, valuation, @@ -1038,21 +1156,8 @@ describe("NFT Pair", async () => { await expect( pair .connect(carol) - .requestAndBorrow( - apeIds.carolOne, - bob.address, - carol.address, - { valuation, duration, annualInterestBPS }, - false, - true, - deadline, - v, - r, - s - ) - ) - .to.emit(pair, "LogRequestLoan") - .to.emit(pair, "LogLend"); + .requestAndBorrow(apeIds.carolOne, bob.address, carol.address, { valuation, duration, annualInterestBPS }, false, true, sigParams) + ).to.emit(pair, "LogLend"); }); it("Should require an exact match on all conditions", async () => { @@ -1062,7 +1167,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: apeIds.carolOne, anyTokenId: false, valuation, @@ -1083,7 +1188,7 @@ describe("NFT Pair", async () => { const altered = BigNumber.from(value).add(1); const badLoanParams = { ...loanParams, [key]: altered }; await expect( - pair.connect(carol).requestAndBorrow(apeIds.carolOne, bob.address, carol.address, badLoanParams, false, false, deadline, v, r, s) + pair.connect(carol).requestAndBorrow(apeIds.carolOne, bob.address, carol.address, badLoanParams, false, false, sigParams) ).to.be.revertedWith("NFTPair: signature invalid"); } }); @@ -1095,7 +1200,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: apeIds.carolOne, anyTokenId: false, valuation, @@ -1107,7 +1212,7 @@ describe("NFT Pair", async () => { const loanParams = { valuation, duration, annualInterestBPS }; // Carol tries to take the loan from Alice instead and fails: await expect( - pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, false, deadline, v, r, s) + pair.connect(carol).requestAndBorrow(apeIds.carolOne, alice.address, carol.address, loanParams, false, false, sigParams) ).to.be.revertedWith("NFTPair: signature invalid"); }); @@ -1118,7 +1223,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: apeIds.carolOne, anyTokenId: false, valuation, @@ -1128,7 +1233,7 @@ describe("NFT Pair", async () => { }); const loanParams = { valuation, duration, annualInterestBPS }; - const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, sigParams] as const; // Request fails because the deadline has expired: await advanceNextTime(3601); @@ -1142,7 +1247,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signLendRequest(bob, { + const sigParams = await signLendRequest(pair, bob, { tokenId: apeIds.carolOne, anyTokenId: false, valuation, @@ -1152,13 +1257,13 @@ describe("NFT Pair", async () => { }); const loanParams = { valuation, duration, annualInterestBPS }; - const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, deadline, v, r, s] as const; + const successParams = [apeIds.carolOne, bob.address, carol.address, loanParams, false, false, sigParams] as const; // It works the first time: await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.emit(pair, "LogLend"); // Carol repays the loan to get the token back: - await expect(pair.connect(carol).repay(apeIds.carolOne, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.carolOne)).to.equal(carol.address); // It fails now (because the nonce is no longer a match): @@ -1183,7 +1288,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signBorrowRequest(bob, { + const sigParams = await signBorrowRequest(pair, bob, { tokenId: apeIds.bobTwo, valuation, duration, @@ -1193,12 +1298,8 @@ describe("NFT Pair", async () => { // Alice takes the loan: await expect( - pair - .connect(alice) - .takeCollateralAndLend(apeIds.bobTwo, bob.address, { valuation, duration, annualInterestBPS }, false, deadline, v, r, s) - ) - .to.emit(pair, "LogRequestLoan") - .to.emit(pair, "LogLend"); + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, { valuation, duration, annualInterestBPS }, false, sigParams) + ).to.emit(pair, "LogLend"); }); it("Should require an exact match on all conditions", async () => { @@ -1208,7 +1309,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signBorrowRequest(bob, { + const sigParams = await signBorrowRequest(pair, bob, { tokenId: apeIds.bobTwo, valuation, duration, @@ -1221,7 +1322,7 @@ describe("NFT Pair", async () => { const altered = BigNumber.from(value).add(1); const badLoanParams = { ...loanParams, [key]: altered }; await expect( - pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, badLoanParams, false, deadline, v, r, s) + pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, badLoanParams, false, sigParams) ).to.be.revertedWith("NFTPair: signature invalid"); } }); @@ -1233,7 +1334,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signBorrowRequest(bob, { + const sigParams = await signBorrowRequest(pair, bob, { tokenId: apeIds.bobTwo, valuation, duration, @@ -1243,9 +1344,9 @@ describe("NFT Pair", async () => { const loanParams = { valuation, duration, annualInterestBPS }; // Alice tries to lend to Carol instead and fails: - await expect( - pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, carol.address, loanParams, false, deadline, v, r, s) - ).to.be.revertedWith("NFTPair: signature invalid"); + await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, carol.address, loanParams, false, sigParams)).to.be.revertedWith( + "NFTPair: signature invalid" + ); }); it("Should enforce the deadline", async () => { @@ -1255,7 +1356,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signBorrowRequest(bob, { + const sigParams = await signBorrowRequest(pair, bob, { tokenId: apeIds.bobTwo, valuation, duration, @@ -1266,9 +1367,9 @@ describe("NFT Pair", async () => { const loanParams = { valuation, duration, annualInterestBPS }; await advanceNextTime(3601); - await expect( - pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s) - ).to.be.revertedWith("NFTPair: signature expired"); + await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, sigParams)).to.be.revertedWith( + "NFTPair: signature expired" + ); }); it("Should not accept the same signature twice", async () => { @@ -1278,7 +1379,7 @@ describe("NFT Pair", async () => { const annualInterestBPS = 15000; const deadline = timestamp + 3600; - const { r, s, v } = await signBorrowRequest(bob, { + const sigParams = await signBorrowRequest(pair, bob, { tokenId: apeIds.bobTwo, valuation, duration, @@ -1288,19 +1389,19 @@ describe("NFT Pair", async () => { const loanParams = { valuation, duration, annualInterestBPS }; - await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s)).to.emit( + await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, sigParams)).to.emit( pair, "LogLend" ); // Bob repays the loan to get the token back: - await expect(pair.connect(bob).repay(apeIds.bobTwo, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(bob).repay(apeIds.bobTwo, bob.address, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.bobTwo)).to.equal(bob.address); // It fails now (because the nonce is no longer a match): - await expect( - pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, deadline, v, r, s) - ).to.be.revertedWith("NFTPair: signature invalid"); + await expect(pair.connect(alice).takeCollateralAndLend(apeIds.bobTwo, bob.address, loanParams, false, sigParams)).to.be.revertedWith( + "NFTPair: signature invalid" + ); }); }); @@ -1401,17 +1502,16 @@ describe("NFT Pair", async () => { }); }); - describeSnapshot("Cook Scenarios", () => { + describeSnapshot("Other Cook Scenarios", () => { // Uses its own let pair: NFTPair; - let chainId: BigNumberish; + let market: NFTMarketMock; let DOMAIN_SEPARATOR: string; let BORROW_SIGNATURE_HASH: string; let LEND_SIGNATURE_HASH: string; const tokenIds: BigNumber[] = []; before(async () => { - chainId = (await ethers.provider.getNetwork()).chainId; pair = await deployPair(); // Undo some of the setup, so we start from scratch: const mc = masterContract.address; @@ -1694,6 +1794,764 @@ describe("NFT Pair", async () => { }); }); + describeSnapshot("Flash Repay", () => { + let pair: NFTPair; + let swapper: NFTBuyerSellerMock; + + const params1 = { + valuation: getBigNumber(10), + duration: YEAR, + annualInterestBPS: 2000, + }; + const params2 = { ...params1, valuation: getBigNumber(25) }; + + before(async () => { + // Alice requests a loan of 10 guineas against "AliceOne": + pair = await deployPair(); + swapper = await deployContract("NFTBuyerSellerMock", bentoBox.address, apesMarket.address); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + // Alice requests two loans + await pair.connect(alice).requestLoan(apeIds.aliceOne, params1, alice.address, false); + await pair.connect(alice).requestLoan(apeIds.aliceTwo, params2, alice.address, false); + + // Bob issues the loans + await pair.connect(bob).lend(apeIds.aliceOne, params1, false); + await pair.connect(bob).lend(apeIds.aliceTwo, params2, false); + + // Alice donates all her money to Carol + await bentoBox + .connect(alice) + .transfer(guineas.address, alice.address, carol.address, await bentoBox.balanceOf(guineas.address, alice.address)); + await guineas.connect(alice).transfer(carol.address, await guineas.balanceOf(alice.address)); + + // Carol uses some of it to fund the apes market + await guineas.connect(carol).approve(apesMarket.address, MaxUint256); + await apesMarket.connect(carol).fund(getBigNumber(100)); + }); + + it("Should allow flash repayments via cook()", async () => { + const getBalances = async () => ({ + alice: await guineas.balanceOf(alice.address), + aliceBento: await bentoBox.balanceOf(guineas.address, alice.address), + + pairBento: await bentoBox.balanceOf(guineas.address, pair.address), + pairFees: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + expect(t0.alice).to.equal(0); + expect(t0.aliceBento).to.equal(0); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // We're just repaying one loan in this test. + + // 1. Repay, send to marketplace to skim. Also, set "skim = true" so that + // the actual payment will be skimmed later. + // This sets both slots of `result`, to [, ]. + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + + // 2. Sell the NFT, by skimming. + // Our mock "market" happens to require a hardcoded amount, because + // `cook()` is not flexible enough to pass the amount needed in the + // appropriate parameter position. + // This will not always be the case, but we have to assume that it is + // in general, so we have not "fixed" the mock contract to make it + // work. That way, this/these test case(s) paint a more fair picture + // of what using `cook()` for flash loans is like. + const salePrice = getBigNumber(11); // enough to cover the loan + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [ + apesMarket.address, + apesMarket.interface.encodeFunctionData("sell", [apeIds.aliceOne, salePrice, alice.address, true]), + false, + false, + 0, + ] + ) + ); + + // 3. ACTION_REPAY told us how much it was; transfer that amount to the + // BentoBox. `result[0]` has the amount in shares: + actions.push(ACTION_BENTO_DEPOSIT); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, pair.address, 0, USE_VALUE1])); + + // That's it; `cook()` will now skim the balance at the end: + await expect(pair.connect(alice).cook(actions, values, datas)).to.emit(pair, "LogRepay"); + + const t1 = await getBalances(); + + // Alice should have a little left: + }); + + it("Should allow multiple flash repayments via cook()", async () => { + const getBalances = async () => ({ + alice: await guineas.balanceOf(alice.address), + aliceBento: await bentoBox.balanceOf(guineas.address, alice.address), + + pairBento: await bentoBox.balanceOf(guineas.address, pair.address), + pairFees: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + expect(t0.alice).to.equal(0); + expect(t0.aliceBento).to.equal(0); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // (See the single repayment test for more function-specific comments) + // What we're doing here is: + // - Flash repay first loan, send token to "market" contract + // - Sell the first token in the market (for "enough"; can't get exact) + // - Flash repay second loan, send token to "market" contract + // - Sell the second token in the market (again, enough)h + // - Pay enough for both loans at once + // + // This makes it even harder to deposit only EXACTLY what was required, + // unless we're willing to needlessly do two Bento transfers. (The repay + // logic populates fields with shares or amount required, but we have + // no way to add them up). + // Since we're testing the double recursion, we leave that aspect as is + // and just send "definitely enough to cover" if we want it to succeed. + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + + const salePrice1 = getBigNumber(11); // enough to cover the loan + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [ + apesMarket.address, + apesMarket.interface.encodeFunctionData("sell", [apeIds.aliceOne, salePrice1, alice.address, true]), + false, + false, + 0, + ] + ) + ); + + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceTwo, apesMarket.address, true])); + + const salePrice2 = getBigNumber(26); // enough to cover the loan + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [ + apesMarket.address, + apesMarket.interface.encodeFunctionData("sell", [apeIds.aliceTwo, salePrice2, alice.address, true]), + false, + false, + 0, + ] + ) + ); + + // 3. ACTION_REPAY told us how much it was; transfer that amount to the + // BentoBox. `result[0]` has the amount in shares: + actions.push(ACTION_BENTO_DEPOSIT); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, pair.address, salePrice1.add(salePrice2), 0])); + + // That's it; `cook()` will now skim the balance at the end: + await expect(pair.connect(alice).cook(actions, values, datas)).to.emit(pair, "LogRepay").to.emit(pair, "LogRepay"); + }); + + it("Should refuse flash repayments via cook() - not enough", async () => { + const getBalances = async () => ({ + alice: await guineas.balanceOf(alice.address), + aliceBento: await bentoBox.balanceOf(guineas.address, alice.address), + + pairBento: await bentoBox.balanceOf(guineas.address, pair.address), + pairFees: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + expect(t0.alice).to.equal(0); + expect(t0.aliceBento).to.equal(0); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // We're just repaying one loan in this test. + + // 1. Repay, send to marketplace to skim. Also, set "skim = true" so that + // the actual payment will be skimmed later. + // This sets both slots of `result`, to [, ]. + actions.push(ACTION_REPAY); + values.push(0); + datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + + // 2. Sell the NFT, by skimming. + // Our mock "market" happens to require a hardcoded amount, because + // `cook()` is not flexible enough to pass the amount needed in the + // appropriate parameter position. + // This will not always be the case, but we have to assume that it is + // in general, so we have not "fixed" the mock contract to make it + // work. That way, this/these test case(s) paint a more fair picture + // of what using `cook()` for flash loans is like. + const salePrice = getBigNumber(11); // enough to cover the loan + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [ + apesMarket.address, + apesMarket.interface.encodeFunctionData("sell", [apeIds.aliceOne, salePrice, alice.address, true]), + false, + false, + 0, + ] + ) + ); + + // 3. Skip sending the money + + // That's it; `cook()` will now skim the balance at the end: + await expect(pair.connect(alice).cook(actions, values, datas)).to.be.revertedWith("NFTPair: skim too much"); + }); + + it("Should allow flash repayments with swapper - good price", async () => { + await expect(pair.connect(alice).flashRepay(apeIds.aliceOne, params1.valuation.mul(2), swapper.address, alice.address, false)).to.emit( + pair, + "LogRepay" + ); + }); + + it("Should refuse flash repayments with swapper - not enough", async () => { + await expect( + pair.connect(alice).flashRepay( + apeIds.aliceOne, + params1.valuation, // Will not cover interest + swapper.address, + alice.address, + false + ) + ).to.be.revertedWith("BoringMath: Underflow"); + }); + }); + + describeSnapshot("Flash Borrow", () => { + let pair: NFTPair; + let swapper: NFTBuyerSellerMock; + + const params1 = { + valuation: getBigNumber(10), + duration: YEAR, + annualInterestBPS: 2000, + }; + const params2 = { ...params1, valuation: getBigNumber(25) }; + + let aliceSig: ISignature; + let bobSig: ISignature; + + before(async () => { + // Alice requests a loan of 10 guineas against "AliceOne": + pair = await deployPair(); + swapper = await deployContract("NFTBuyerSellerMock", bentoBox.address, apesMarket.address); + + for (const signer of [deployer, alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(pair.address, true); + } + + const { timestamp } = await ethers.provider.getBlock("latest"); + const deadline = timestamp + 3600; + + // Alice signs a lending request against CarolOne + aliceSig = await signLendRequest(pair, alice, { + tokenId: apeIds.carolOne, + anyTokenId: false, + ...params1, + deadline, + }); + // Bob signs a lending request against any ape + bobSig = await signLendRequest(pair, bob, { + tokenId: 0, + anyTokenId: true, + ...params2, + deadline, + }); + + // Stock the apes market: + for (const signer of [alice, bob, carol]) { + await apes.connect(signer).setApprovalForAll(apesMarket.address, true); + } + await apesMarket.connect(alice).stock(apeIds.aliceOne); + await apesMarket.connect(bob).stock(apeIds.bobOne); + await apesMarket.connect(carol).stock(apeIds.carolOne); + }); + + it("Should allow flash borrowing (LTV < 1)", async () => { + // Bob knows about Alice's commitment to lend 10 guineas against ape + // "carolOne". Assuming it costs 13 guineas, he needs to put up another + // 3, plus the opening fee on borrowing the 10 -- 0.1 guinea: + const price = getBigNumber(13); + // `toShare` rounding up: + const priceShare = price.mul(9).add(19).div(20); // rounding up + const totalShare = params1.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + const shortage = priceShare.sub(borrowerShare); + await expect( + pair + .connect(bob) + .flashRequestAndBorrow(apeIds.carolOne, alice.address, bob.address, params1, false, aliceSig, price, swapper.address, false) + ) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, bob.address, pair.address, shortage) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, swapper.address, price) + .to.emit(guineas, "Transfer") + .withArgs(swapper.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, pair.address, apeIds.carolOne); + }); + + it("Should allow flash borrowing (LTV > 1)", async () => { + // Bob can buy "CarolOne" at a price below what he can borrow against it. + // As such he can get paid to take out the loab: + // "carolOne". Assuming it costs 13 guineas, he needs to put up another + // 3, plus the opening fee on borrowing the 10 -- 0.1 guinea: + const price = getBigNumber(7); + // `toShare` rounding up: + const priceShare = price.mul(9).add(19).div(20); // rounding up + const totalShare = params1.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + const excess = borrowerShare.sub(priceShare); + await expect( + pair + .connect(bob) + .flashRequestAndBorrow(apeIds.carolOne, alice.address, bob.address, params1, false, aliceSig, price, swapper.address, false) + ) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, bob.address, excess) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, swapper.address, price) + .to.emit(guineas, "Transfer") + .withArgs(swapper.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, pair.address, apeIds.carolOne); + }); + + it("Should allow flash borrowing (LTV = 1)", async () => { + const getBalances = async () => ({ + bob: await guineas.balanceOf(bob.address), + bobBento: await bentoBox.balanceOf(guineas.address, bob.address), + + deployer: await guineas.balanceOf(deployer.address), + deployerBento: await bentoBox.balanceOf(guineas.address, deployer.address), + }); + const t0 = await getBalances(); + + // Bob can buy "CarolOne" at a price that happens to be exactly what he + // gets when taking out a loan -- meaning the principal less open fee. + // Actually, given the signature, anyone can make the call; we'll let the + // deployer do it. Bob still gets the loan: + const totalShare = params1.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + + const priceShare = borrowerShare; + // The following mght be off a little, if the amount/share ratio is not + // hardcoded to 20/9: + const price = priceShare.mul(20).div(9); + + await expect( + pair + .connect(deployer) + .flashRequestAndBorrow(apeIds.carolOne, alice.address, bob.address, params1, false, aliceSig, price, swapper.address, false) + ) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, swapper.address, price) + .to.emit(guineas, "Transfer") + .withArgs(swapper.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, pair.address, apeIds.carolOne); + + const t1 = await getBalances(); + + expect(t1.deployer.sub(t0.deployer)).to.equal(0); + expect(t1.deployerBento.sub(t0.deployerBento)).to.equal(0); + expect(t1.bob.sub(t0.bob)).to.equal(0); + expect(t1.bobBento.sub(t0.bobBento)).to.equal(0); + }); + + it("Should allow flash borrowing (LTV < 1, cook)", async () => { + // Bob knows about Alice's commitment to lend 10 guineas against ape + // "carolOne". Assuming it costs 13 guineas, he needs to put up another + // 3, plus the opening fee on borrowing the 10 -- 0.1 guinea: + const price = getBigNumber(13); + // `toShare` rounding up: + const priceShare = price.mul(9).add(19).div(20); // rounding up + const totalShare = params1.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + const shortage = priceShare.sub(borrowerShare); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // Bob starts the test with JUST enough guineas to make up the shortage: + await bentoBox + .connect(bob) + .transfer(guineas.address, bob.address, deployer.address, (await bentoBox.balanceOf(guineas.address, bob.address)).sub(shortage)); + + // Bob requests a loan agains CarolOne, uses that loan to buy help buy it + // at the market, then posts the purchased token as collateral. + + // 1. Take out the loan + actions.push(ACTION_REQUEST_AND_BORROW); + values.push(0); + datas.push( + encodeParameters( + [ + "uint256", + "address", + "address", + "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", + "bool", + "bool", + "tuple(uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + ], + [ + apeIds.carolOne, + alice.address, + bob.address, + params1, + true, // (skimCOllateral) + false, + aliceSig, + ] + ) + ); + + // 2. Send funds to the apes market. Since Bob now has the loan in his + // BentoBox balance, this will succeed: + actions.push(ACTION_BENTO_WITHDRAW); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, apesMarket.address, price, 0])); + + // 3. Buy the token at the apes market, skimming the funds and sending + // the token to the pair: + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [apesMarket.address, apesMarket.interface.encodeFunctionData("buy", [apeIds.carolOne, price, pair.address, true]), false, false, 0] + ) + ); + + // .. that's it; having indicated in step 1 that we are skimming the + // token, `cook()` will use it as collateral in the final step. + await expect(pair.connect(bob).cook(actions, values, datas)) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, bob.address, borrowerShare) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, alice.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, pair.address, apeIds.carolOne); + }); + + it("Should allow flash borrowing (LTV > 1, cook)", async () => { + // Alice borrows 25 guineas from Bob, using his "any token" signature, + // against "carolOne". Assuming it costs 20 guineas, Alice can start with + // no guineas at all, and end up with about 4.75 guineas -- the 5 extra, + // minus the 0.25 open fee on the loan -- along with the option to repay + // the loan. + const price = getBigNumber(20); + const priceShare = price.mul(9).add(19).div(20); // rounding up + const totalShare = params2.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + const excessShare = borrowerShare.sub(priceShare); + + expect(excessShare).to.be.lt(getBigNumber(5).mul(9).div(20)); + expect(excessShare).to.be.gte(getBigNumber(475).div(100).mul(9).div(20)); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // Alice starts the test with an empty BentoBox balance: + await bentoBox + .connect(alice) + .transfer(guineas.address, alice.address, deployer.address, await bentoBox.balanceOf(guineas.address, alice.address)); + + // Alice requests a loan agains CarolOne, uses some of the loan to buy it + // at the market, then posts the purchased token as collateral. + + // 1. Take out the loan + actions.push(ACTION_REQUEST_AND_BORROW); + values.push(0); + datas.push( + encodeParameters( + [ + "uint256", + "address", + "address", + "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", + "bool", + "bool", + "tuple(uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + ], + [ + apeIds.carolOne, + bob.address, + alice.address, + params2, + true, // (skimCOllateral) + true, // (anyTokenId in sig) + bobSig, + ] + ) + ); + + // 2. Send funds to the apes market. Succeeds, since Alice got the loan: + actions.push(ACTION_BENTO_WITHDRAW); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, apesMarket.address, price, 0])); + + // 3. Buy the token at the apes market, skimming the funds and sending + // the token to the pair: + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [apesMarket.address, apesMarket.interface.encodeFunctionData("buy", [apeIds.carolOne, price, pair.address, true]), false, false, 0] + ) + ); + + // .. that's it; having indicated in step 1 that we are skimming the + // token, `cook()` will use it as collateral in the final step. + await expect(pair.connect(alice).cook(actions, values, datas)) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, alice.address, borrowerShare) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, bob.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, pair.address, apeIds.carolOne); + + expect(await bentoBox.balanceOf(guineas.address, alice.address)).to.equal(excessShare); + }); + + it("Should reject if the collateral is not posted (cook)", async () => { + // Uses the successful case of 25 against Bob's "any" signature, but + // fails to send the token to the contract + const price = getBigNumber(20); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // Alice starts the test with an empty BentoBox balance: + await bentoBox + .connect(alice) + .transfer(guineas.address, alice.address, deployer.address, await bentoBox.balanceOf(guineas.address, alice.address)); + + // Alice requests a loan agains CarolOne, uses some of the loan to buy it + // at the market. The collateral stays with Alice this time: + + // 1. Take out the loan + actions.push(ACTION_REQUEST_AND_BORROW); + values.push(0); + datas.push( + encodeParameters( + [ + "uint256", + "address", + "address", + "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", + "bool", + "bool", + "tuple(uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + ], + [ + apeIds.carolOne, + bob.address, + alice.address, + params2, + true, // (skimCOllateral) + true, // (anyTokenId in sig) + bobSig, + ] + ) + ); + + // 2. Send funds to the apes market. Succeeds, since Alice got the loan: + actions.push(ACTION_BENTO_WITHDRAW); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, apesMarket.address, price, 0])); + + // 3. Buy the token at the apes market, skimming the funds and sending + // the token to the pair: + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [ + apesMarket.address, + apesMarket.interface.encodeFunctionData("buy", [ + apeIds.carolOne, + price, + alice.address, // not pair.address + true, + ]), + false, + false, + 0, + ] + ) + ); + + // .. that's it; having indicated in step 1 that we are skimming the + // token, `cook()` will use it as collateral in the final step. + await expect(pair.connect(alice).cook(actions, values, datas)).to.be.revertedWith("NFTPair: skim failed"); + }); + + it("Should allow flash borrowing (LTV > 1, cook, no skim)", async () => { + // Skimming is more efficient, but if Alice has approved the contract + // then the collateral can be taken from her address instead. + // This is the same as the "reject if collateral not posted" scenario, + // where Alice ends up with the collateral, except that skimming is not + // used. As a last step, the pair takes the collateral from Alice: + const price = getBigNumber(20); + const priceShare = price.mul(9).add(19).div(20); // rounding up + const totalShare = params2.valuation.mul(9).div(20); // rounding down + const openFeeShare = totalShare.mul(1).div(100); + const protocolFeeShare = openFeeShare.div(10); + const lenderOutShare = totalShare.sub(openFeeShare).add(protocolFeeShare); + const borrowerShare = totalShare.sub(openFeeShare); + const excessShare = borrowerShare.sub(priceShare); + + expect(excessShare).to.be.lt(getBigNumber(5).mul(9).div(20)); + expect(excessShare).to.be.gte(getBigNumber(475).div(100).mul(9).div(20)); + + const actions: number[] = []; + const values: any[] = []; + const datas: any[] = []; + + // Alice starts the test with an empty BentoBox balance: + await bentoBox + .connect(alice) + .transfer(guineas.address, alice.address, deployer.address, await bentoBox.balanceOf(guineas.address, alice.address)); + + // Alice requests a loan agains CarolOne, uses some of the loan to buy it + // at the market, then posts the purchased token as collateral. + + // 1. Take out the loan + actions.push(ACTION_REQUEST_AND_BORROW); + values.push(0); + datas.push( + encodeParameters( + [ + "uint256", + "address", + "address", + "tuple(uint128 valuation, uint64 duration, uint16 annualInterestBPS)", + "bool", + "bool", + "tuple(uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + ], + [ + apeIds.carolOne, + bob.address, + alice.address, + params2, + false, // (skimCOllateral) + true, // (anyTokenId in sig) + bobSig, + ] + ) + ); + + // 2. Send funds to the apes market. Succeeds, since Alice got the loan: + actions.push(ACTION_BENTO_WITHDRAW); + values.push(0); + datas.push(encodeParameters(["address", "address", "int256", "int256"], [guineas.address, apesMarket.address, price, 0])); + + // 3. Buy the token at the apes market, skimming the funds and sending + // the token to the pair: + actions.push(ACTION_CALL); + values.push(0); + datas.push( + encodeParameters( + ["address", "bytes", "bool", "bool", "uint8"], + [apesMarket.address, apesMarket.interface.encodeFunctionData("buy", [apeIds.carolOne, price, alice.address, true]), false, false, 0] + ) + ); + + // .. that's it: the contract will take the collateral from Alice, who + // got it from the apes market. + await expect(pair.connect(alice).cook(actions, values, datas)) + .to.emit(pair, "LogLend") + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, pair.address, alice.address, borrowerShare) + .to.emit(bentoBox, "LogTransfer") + .withArgs(guineas.address, bob.address, pair.address, lenderOutShare) + .to.emit(guineas, "Transfer") + .withArgs(bentoBox.address, apesMarket.address, price) + .to.emit(apes, "Transfer") + .withArgs(apesMarket.address, alice.address, apeIds.carolOne) + .to.emit(apes, "Transfer") + .withArgs(alice.address, pair.address, apeIds.carolOne); + + expect(await bentoBox.balanceOf(guineas.address, alice.address)).to.equal(excessShare); + }); + }); + describeSnapshot("Lending Club", () => { let pair: NFTPair; let lendingClub: LendingClubMock; @@ -1716,8 +2574,14 @@ describe("NFT Pair", async () => { await bentoBox.connect(bob).deposit(guineas.address, bob.address, lendingClub.address, getBigNumber(10), 0); }); - const borrow = (borrower: SignerWithAddress, club: LendingClubMock, tokenId: BigNumberish, params: ILoanParams) => - pair.connect(borrower).requestAndBorrow(tokenId, club.address, borrower.address, params, false, false, 0, 0, HashZero, HashZero); + const borrow = ( + borrower: SignerWithAddress, + club: LendingClubMock, + tokenId: BigNumberish, + params: ILoanParams, + // Defaults to "next week' + deadline: BigNumberish = Math.floor(new Date().getTime() / 1000) + 86400 * 7 + ) => pair.connect(borrower).requestAndBorrow(tokenId, club.address, borrower.address, params, false, false, zeroSign(deadline)); it("Should allow LendingClubs to approve or reject loans", async () => { // Mock implementation detail: tokenId has to be even @@ -1741,9 +2605,7 @@ describe("NFT Pair", async () => { duration, annualInterestBPS, }) - ) - .to.emit(pair, "LogRequestLoan") - .to.emit(pair, "LogLend"); + ).to.emit(pair, "LogLend"); expect(getBigNumber(apeIds.aliceTwo, 0).mod(2)).to.equal(1); await expect( From 60d4e1cbdd56fed48c38e7ee2b0b83e4c8f5e6ba Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 5 Jun 2022 08:06:55 +0200 Subject: [PATCH 088/107] Only expire loans AFTER duration has passed --- contracts/NFTPair.sol | 4 ++-- test/NFTPair.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 35723941..256a7cc6 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -238,7 +238,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { require(to == loan.lender || msg.sender == loan.lender, "NFTPair: not the lender"); require( // Addition is safe: both summands are smaller than 256 bits - uint256(loan.startTime) + tokenLoanParams[tokenId].duration <= block.timestamp, + uint256(loan.startTime) + tokenLoanParams[tokenId].duration < block.timestamp, "NFTPair: not expired" ); } @@ -590,7 +590,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { TokenLoanParams memory loanParams = tokenLoanParams[tokenId]; require( // Addition is safe: both summands are smaller than 256 bits - uint256(loan.startTime) + loanParams.duration > block.timestamp, + uint256(loan.startTime) + loanParams.duration >= block.timestamp, "NFTPair: loan expired" ); diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 25a4e019..ebb4f171 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -649,7 +649,7 @@ describe("NFT Pair", async () => { }); it("Should allow lenders to seize collateral at expiry", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); // Send it to someone else for a change: await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)) .to.emit(pair, "LogRemoveCollateral") @@ -659,7 +659,7 @@ describe("NFT Pair", async () => { }); it("Should allow lenders to seize collateral after expiry", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 2]); // Send it to someone else for a change: await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)) .to.emit(pair, "LogRemoveCollateral") @@ -669,12 +669,12 @@ describe("NFT Pair", async () => { }); it("Should not allow lenders to seize collateral otherwise", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); await expect(pair.connect(bob).removeCollateral(apeIds.aliceOne, bob.address)).to.be.revertedWith("NFTPair: not expired"); }); it("Should not allow others to seize collateral ever", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration - 1]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); await expect(pair.connect(carol).removeCollateral(apeIds.aliceOne, carol.address)).to.be.revertedWith("NFTPair: not the lender"); await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1_000_000]); @@ -1056,12 +1056,12 @@ describe("NFT Pair", async () => { }); it("Should refuse repayments on expired loans", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, false)).to.be.revertedWith("NFTPair: loan expired"); }); it("Should refuse repayments on nonexistent loans", async () => { - await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration]); + await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, false)).to.be.revertedWith("NFTPair: no loan"); }); From bf367dfc4575b297e04df2b8e8bd012294726454 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:49:37 +0200 Subject: [PATCH 089/107] Include more detail in events --- contracts/NFTPair.sol | 26 +++++++++++----------- test/NFTPair.test.ts | 51 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 256a7cc6..de7dc428 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -40,12 +40,12 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, TokenLoanParams params); + event LogUpdateLoanParams(uint256 indexed tokenId, TokenLoanParams params); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request - event LogLend(address indexed lender, uint256 indexed tokenId); + event LogLend(address indexed lender, address indexed borrower, uint256 indexed tokenId, TokenLoanParams params); event LogRepay(address indexed from, uint256 indexed tokenId); event LogFeeTo(address indexed newFeeTo); event LogWithdrawFees(address indexed feeTo, uint256 feeShare); @@ -177,7 +177,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS); + emit LogUpdateLoanParams(tokenId, params); } /// @notice It is the caller's responsibility to ensure skimmed tokens get accounted for somehow so they cannot be used twice. @@ -215,7 +215,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { tokenLoan[tokenId] = loan; tokenLoanParams[tokenId] = params; - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS); + emit LogRequestLoan(to, tokenId, params); // Skimming is safe: // - This method both requires loan state to be LOAN_INITIAL and sets // it to something else. Every other use of _requireCollateral must @@ -258,10 +258,10 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address borrower, address initialRecipient, uint256 tokenId, - uint256 amount, + TokenLoanParams memory params, bool skim ) internal returns (uint256 borrowerShare) { - uint256 totalShare = bentoBox.toShare(asset, amount, false); + uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); // No overflow: at most 128 + 16 bits (fits in BentoBox) uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; @@ -287,7 +287,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; - emit LogLend(lender, tokenId); + emit LogLend(lender, borrower, tokenId, params); } /// @notice Lends with the parameters specified by the borrower. @@ -311,7 +311,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { requested.annualInterestBPS >= accepted.annualInterestBPS, "NFTPair: bad params" ); - _lend(msg.sender, loan.borrower, loan.borrower, tokenId, requested.valuation, skim); + _lend(msg.sender, loan.borrower, loan.borrower, tokenId, requested, skim); } // solhint-disable-next-line func-name-mixedcase @@ -348,7 +348,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { SignatureParams memory signature ) public { _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); - _lend(lender, borrower, borrower, tokenId, params.valuation, false); + _lend(lender, borrower, borrower, tokenId, params, false); // Skimming is safe: // - This method both requires loan state to be LOAN_INITIAL and sets // it to something else. Every other use of _requireCollateral must @@ -376,7 +376,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // Bento-shares received by taking out the loan. They are sent to the // buyer contract for skimming. // TODO: Allow Bento-withdrawing instead? - uint256 borrowerShare = _lend(lender, borrower, address(this), tokenId, params.valuation, false); + uint256 borrowerShare = _lend(lender, borrower, address(this), tokenId, params, false); // At this point the contract has `borrowerShare` extra shares. If this // is too much, then the borrower gets the excess. If this is not // enough, we either take the rest from msg.sender, or have the amount @@ -482,7 +482,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { SignatureParams memory signature ) public { _requireSignedBorrowParams(borrower, tokenId, params, signature); - _lend(msg.sender, borrower, borrower, tokenId, params.valuation, skimFunds); + _lend(msg.sender, borrower, borrower, tokenId, params, skimFunds); // Skimming is safe: // - This method both requires loan state to be LOAN_INITIAL and sets // it to something else. Every other use of _requireCollateral must @@ -892,7 +892,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { (uint256, address, address, TokenLoanParams, bool, bool, SignatureParams) ); _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); - _lend(lender, borrower, borrower, tokenId, params.valuation, false); + _lend(lender, borrower, borrower, tokenId, params, false); } _cook(actions, values, datas, ++i, result); // Skimming is safe: diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index ebb4f171..6e30fd49 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -12,6 +12,8 @@ const hashUtf8String = (s: string) => keccak256(toUtf8Bytes(s)); const zeroSign = (deadline) => ({ r: HashZero, s: HashZero, v: 0, deadline }); +const paramsArray = (params: ILoanParams) => [params.valuation, params.duration, params.annualInterestBPS]; + import { BigRational, advanceNextTime, duration, encodeParameters, expApprox, getBigNumber, impersonate } from "../utilities"; import { BentoBoxMock, ERC20Mock, ERC721Mock, LendingClubMock, NFTMarketMock, NFTBuyerSellerMock, WETH9Mock, NFTPair } from "../typechain"; import { describeSnapshot } from "./helpers"; @@ -319,7 +321,7 @@ describe("NFT Pair", async () => { .to.emit(apes, "Transfer") .withArgs(alice.address, pair.address, apeIds.aliceOne) .to.emit(pair, "LogRequestLoan") - .withArgs(alice.address, apeIds.aliceOne, params.valuation, params.duration, params.annualInterestBPS); + .withArgs(alice.address, apeIds.aliceOne, paramsArray(params)); }); it("Should let anyone with an NFT request a loan (skim)", async () => { @@ -416,7 +418,7 @@ describe("NFT Pair", async () => { await expect(pair.connect(carol).lend(apeIds.aliceOne, params1, false)) .to.emit(pair, "LogLend") - .withArgs(carol.address, apeIds.aliceOne) + .withArgs(carol.address, alice.address, apeIds.aliceOne, paramsArray(params1)) .to.emit(bentoBox, "LogTransfer") .withArgs(guineas.address, carol.address, pair.address, lenderOut) .to.emit(bentoBox, "LogTransfer") @@ -449,7 +451,7 @@ describe("NFT Pair", async () => { // Loan was requested by Bob, but money and option to repay go to Carol: await expect(pair.connect(alice).lend(apeIds.bobTwo, params1, false)) .to.emit(pair, "LogLend") - .withArgs(alice.address, apeIds.bobTwo) + .withArgs(alice.address, carol.address, apeIds.bobTwo, paramsArray(params1)) .to.emit(bentoBox, "LogTransfer") .withArgs(guineas.address, alice.address, pair.address, lenderOut) .to.emit(bentoBox, "LogTransfer") @@ -542,7 +544,7 @@ describe("NFT Pair", async () => { for (const params of data) { await expect(pair.connect(alice).updateLoanParams(apeIds.aliceOne, params)) .to.emit(pair, "LogUpdateLoanParams") - .withArgs(apeIds.aliceOne, params.valuation, params.duration, params.annualInterestBPS); + .withArgs(apeIds.aliceOne, paramsArray(params)); } }); @@ -1660,11 +1662,35 @@ describe("NFT Pair", async () => { .to.emit(apes, "Transfer") .withArgs(bob.address, pair.address, tokenIds[9]) .to.emit(pair, "LogRequestLoan") - .withArgs(alice.address, tokenIds[6], getBigNumber(7 * 12), YEAR, 6 * 500) + .withArgs( + alice.address, + tokenIds[6], + paramsArray({ + valuation: getBigNumber(7 * 12), + duration: YEAR, + annualInterestBPS: 6 * 500, + }) + ) .to.emit(pair, "LogRequestLoan") - .withArgs(bob.address, tokenIds[7], getBigNumber(8 * 12), YEAR, 7 * 500) + .withArgs( + bob.address, + tokenIds[7], + paramsArray({ + valuation: getBigNumber(8 * 12), + duration: YEAR, + annualInterestBPS: 7 * 500, + }) + ) .to.emit(pair, "LogRequestLoan") - .withArgs(carol.address, tokenIds[8], getBigNumber(9 * 12), YEAR, 8 * 500); + .withArgs( + carol.address, + tokenIds[8], + paramsArray({ + valuation: getBigNumber(9 * 12), + duration: YEAR, + annualInterestBPS: 8 * 500, + }) + ); }); it("Should have the expected domain separator", async () => { @@ -1728,7 +1754,16 @@ describe("NFT Pair", async () => { ]) ) .to.emit(pair, "LogLend") - .withArgs(alice.address, tokenIds[0]); + .withArgs( + alice.address, + bob.address, + tokenIds[0], + paramsArray({ + valuation: getBigNumber(12), + duration: YEAR, + annualInterestBPS: 0, + }) + ); }); it("Should revert the entire cook if one transaction fails", async () => { From a349016149b2d5d22b71228d4f36f7d32c7dba01 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:49:45 +0200 Subject: [PATCH 090/107] (Shorten revert message) --- contracts/NFTPair.sol | 2 +- test/NFTPair.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index de7dc428..35c1bcfa 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -419,7 +419,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { SignatureParams memory signature ) private { if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); + require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub refused"); } else { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 6e30fd49..44f75cca 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -2632,7 +2632,7 @@ describe("NFT Pair", async () => { duration, annualInterestBPS, }) - ).to.be.revertedWith("NFTPair: LendingClub does not like you"); + ).to.be.revertedWith("NFTPair: LendingClub refused"); await expect( borrow(alice, lendingClub, apeIds.aliceOne, { @@ -2649,7 +2649,7 @@ describe("NFT Pair", async () => { duration, annualInterestBPS, }) - ).to.be.revertedWith("NFTPair: LendingClub does not like you"); + ).to.be.revertedWith("NFTPair: LendingClub refused"); }); }); }); From bf38d46f47d263c4714749b61d6981d98b43279d Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:49:53 +0200 Subject: [PATCH 091/107] (Add revert message to interest calc) --- contracts/NFTPair.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 35c1bcfa..64acda24 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -570,7 +570,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } if (interest >= 2**128) { - revert(); + revert("NFTPair: Interest unpayable"); } } From f6555a1612fae78ee3117c8c8a6c4a53a22adc51 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:50:01 +0200 Subject: [PATCH 092/107] (Spelling transfered -> transferred) --- contracts/NFTPair.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 64acda24..520bf4e9 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -473,7 +473,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @param tokenId ID of the token that will function as collateral /// @param borrower Address that provides collateral and receives the loan /// @param params Loan terms offered, and signed by the borrower - /// @param skimFunds True if the funds have been transfered to the contract + /// @param skimFunds True if the funds have been transferred to the contract function takeCollateralAndLend( uint256 tokenId, address borrower, @@ -625,7 +625,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // if that exceeds 128 bits the BentoBox transfer will revert. It // follows that `totalShare` fits in 129 bits, and `feesEarnedShare` // fits in 128 as it represents a BentoBox balance. - // Skimming is safe: the amount gets transfered to the lender later, + // Skimming is safe: the amount gets transferred to the lender later, // and therefore cannot be skimmed twice. if (skim) { require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); @@ -903,7 +903,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // Delaying until after the rest of the cook is safe: // - If the rest of the cook _also_ takes this collateral // somehow -- either via skimming, or via just having it - // transfered in -- then it did so by opening a loan. But + // transferred in -- then it did so by opening a loan. But // that is only possible if this one (that we are collecting // the collateral for) got repaid in the mean time, which is // a silly thing to do, but otherwise legitimate and not an From db06bf973c484a0b7f5ad9dbc0ca913e1eb5bae4 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:50:10 +0200 Subject: [PATCH 093/107] Fix comments; fix `_repayBefore()` accessibility --- contracts/NFTPair.sol | 71 +++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 520bf4e9..2aa2c8f8 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -156,6 +156,8 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { require(address(collateral) != address(0), "NFTPair: bad pair"); } + /// @param tokenId The token ID of the loan in question + /// @param params The desired new loan parameters function updateLoanParams(uint256 tokenId, TokenLoanParams memory params) external { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { @@ -293,7 +295,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @notice Lends with the parameters specified by the borrower. /// @param tokenId ID of the token that will function as collateral /// @param accepted Loan parameters as the lender saw them, for security - /// @param skim True if the funds have been transferred to the contract + /// @param skim True if the funds have been Bento-transferred to the contract function lend( uint256 tokenId, TokenLoanParams memory accepted, @@ -321,7 +323,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // NOTE on signature hashes: the domain separator only guarantees that the // chain ID and master contract are a match, so we explicitly include the - // clone address (and the asset/collateral addresses): + // clone address // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint256 nonce,uint256 deadline)") bytes32 private constant LEND_SIGNATURE_HASH = 0x06bcca6f35b7c1b98f11abbb10957d273a681069ba90358de25404f49e2430f8; @@ -338,6 +340,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @param params Loan parameters requested, and signed by the lender /// @param skimCollateral True if the collateral has already been transferred /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + /// @param signature (deadline, v, r, s) of signature. (See docs) function requestAndBorrow( uint256 tokenId, address lender, @@ -357,7 +360,17 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { _requireCollateral(msg.sender, tokenId, skimCollateral); } - ///@param borrower Also receives excess if token cheaper than loan amount + /// @notice Request and immediately borrow from a pre-committed lender, while buying the collateral in the same transaction. + /// @notice Caller provides extra funds if needed; loan can go to a different address. + /// @param tokenId ID of the token that will function as collateral + /// @param lender Lender, whose BentoBox balance the funds will come from + /// @param borrower Receives the funds (and excess if token is cheaper) + /// @param params Loan parameters requested, and signed by the lender + /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + /// @param signature (deadline, v, r, s) of signature. (See docs) + /// @param price Price of token (in wei), sent to buyer contract + /// @param buyer INFTBuyer contract that will purchase the token + /// @param skimShortage True if any funds needed in excess of the loan have already been Bento-transfered to the contract function flashRequestAndBorrow( uint256 tokenId, address lender, @@ -473,7 +486,8 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @param tokenId ID of the token that will function as collateral /// @param borrower Address that provides collateral and receives the loan /// @param params Loan terms offered, and signed by the borrower - /// @param skimFunds True if the funds have been transferred to the contract + /// @param skimFunds True if the funds have been Bento-transferred to the contract + /// @param signature (deadline, v, r, s) of signature. (See docs) function takeCollateralAndLend( uint256 tokenId, address borrower, @@ -493,23 +507,26 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { _requireCollateral(borrower, tokenId, false); } - /// Approximates continuous compounding. Uses Horner's method to evaluate - /// the truncated Maclaurin series for exp - 1, accumulating rounding - /// errors along the way. The following is always guaranteed: - /// - /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), - /// - /// where time = t/YEAR, up to at most the rounding error obtained in - /// calculating linear interest. - /// - /// If the theoretical result that we are approximating (the rightmost part - /// of the above inquality) fits in 128 bits, then the function is - /// guaranteed not to revert (unless n > 250, which is way too high). - /// If even the linear interest (leftmost part of the inequality) does not - /// the function will revert. - /// Otherwise, the function may revert, return a reasonable result, or - /// return a very inaccurate result. Even then the above inequality is - /// respected. + // Approximates continuous compounding. Uses Horner's method to evaluate + // the truncated Maclaurin series for exp - 1, accumulating rounding + // errors along the way. The following is always guaranteed: + // + // principal * time * apr <= result <= principal * (e^(time * apr) - 1), + // + // where time = t/YEAR, up to at most the rounding error obtained in + // calculating linear interest. + // + // If the theoretical result that we are approximating (the rightmost part + // of the above inquality) fits in 128 bits, then the function is + // guaranteed not to revert (unless n > 250, which is way too high). + // If even the linear interest (leftmost part of the inequality) does not + // the function will revert. + // Otherwise, the function may revert, return a reasonable result, or + // return a very inaccurate result. Even then the above inequality is + // respected. + /// @param principal Amount owed in wei + /// @param t Duration in seconds + /// @param aprBPS Annual rate in basis points (1/10_000) function calculateInterest( uint256 principal, uint64 t, @@ -575,7 +592,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } function _repayBefore(uint256 tokenId, address to) - public + private returns ( uint256 totalShare, uint256 totalAmount, @@ -639,6 +656,10 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { emit LogRepay(skim ? address(this) : msg.sender, tokenId); } + /// @notice Repay a loan in full + /// @param tokenId Token ID of the loan in question. + /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. + /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function repay( uint256 tokenId, address to, @@ -648,6 +669,12 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { _repayAfter(lender, totalShare, feeShare, tokenId, skim); } + /// @notice Repay a loan in full, by selling the token in the same transaction. Must be the borrower. + /// @param tokenId Token ID of the loan in question. + /// @param price Sale price of the token, in wei + /// @param seller INFTSeller contract that will perform the sale + /// @param excessRecipient Receives any funds left over after repaying, if any + /// @param skimShortage True if any extra funds required have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function flashRepay( uint256 tokenId, uint256 price, From 394e82a80c52e6bf96ba958c46cc7e38a873c6f0 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:50:18 +0200 Subject: [PATCH 094/107] Clear loan params whenever loan is cleared --- contracts/NFTPair.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 2aa2c8f8..6c60ec00 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -248,6 +248,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // can claim it by first requesting a loan with `skim` set to true, and // then withdrawing. So we might as well allow it here.. delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; collateral.transferFrom(address(this), to, tokenId); emit LogRemoveCollateral(tokenId, to); } From 12f4bf5aaf12be23c6a3dd5b636f2a098c603b53 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:54:01 +0200 Subject: [PATCH 095/107] BringNFTPairWithOracle up to parity --- contracts/NFTPairWithOracle.sol | 651 ++++++++++++------ .../interfaces/ILendingClubWithOracle.sol | 20 + contracts/interfaces/INFTPair.sol | 8 +- contracts/interfaces/INFTPairWithOracle.sol | 31 + contracts/interfaces/SignatureParams.sol | 10 + 5 files changed, 513 insertions(+), 207 deletions(-) create mode 100644 contracts/interfaces/ILendingClubWithOracle.sol create mode 100644 contracts/interfaces/INFTPairWithOracle.sol create mode 100644 contracts/interfaces/SignatureParams.sol diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 5c702304..13ddeb60 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -26,29 +26,10 @@ import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; -import "./interfaces/INFTOracle.sol"; - -struct TokenLoanParamsWithOracle { - uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 duration; // Length of loan in seconds - uint16 annualInterestBPS; // Variable cost of taking out the loan - uint16 ltvBPS; // Required to avoid liquidation - INFTOracle oracle; // oracle used -} - -struct SignatureParams { - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; -} - -interface ILendingClubWithOracle { - // Per token settings. - function willLend(uint256 tokenId, TokenLoanParamsWithOracle memory params) external view returns (bool); - - function lendingConditions(address nftPair, uint256 tokenId) external view returns (TokenLoanParamsWithOracle memory); -} +import "./interfaces/ILendingClubWithOracle.sol"; +import "./interfaces/INFTBuyer.sol"; +import "./interfaces/INFTSeller.sol"; +import "./interfaces/INFTPairWithOracle.sol"; /// @title NFTPairWithOracle /// @dev This contract allows contract calls to any contract (except BentoBox) @@ -59,19 +40,12 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { using RebaseLibrary for Rebase; using BoringERC20 for IERC20; - event LogRequestLoan( - address indexed borrower, - uint256 indexed tokenId, - uint128 valuation, - uint64 duration, - uint16 annualInterestBPS, - uint16 ltvBPS - ); - event LogUpdateLoanParams(uint256 indexed tokenId, uint128 valuation, uint64 duration, uint16 annualInterestBPS, uint16 ltvBPS); + event LogRequestLoan(address indexed borrower, uint256 indexed tokenId, TokenLoanParamsWithOracle params); + event LogUpdateLoanParams(uint256 indexed tokenId, TokenLoanParamsWithOracle params); // This automatically clears the associated loan, if any event LogRemoveCollateral(uint256 indexed tokenId, address recipient); // Details are in the loan request - event LogLend(address indexed lender, uint256 indexed tokenId); + event LogLend(address indexed lender, address indexed borrower, uint256 indexed tokenId, TokenLoanParamsWithOracle params); event LogRepay(address indexed from, uint256 indexed tokenId); event LogFeeTo(address indexed newFeeTo); event LogWithdrawFees(address indexed feeTo, uint256 feeShare); @@ -115,7 +89,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 private constant YEAR_BPS = 3600 * 24 * 365 * 10_000; // Highest order term in the Maclaurin series for exp used by - // `calculateIntest`. + // `calculateInterest`. // Intuitive interpretation: interest continuously accrues on the principal. // That interest, in turn, earns "second-order" interest-on-interest, which // itself earns "third-order" interest, etc. This constant determines how @@ -176,13 +150,15 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } /// @notice De facto constructor for clone contracts - function init(bytes calldata data) public payable override { + function init(bytes calldata data) external payable override { require(address(collateral) == address(0), "NFTPair: already initialized"); (collateral, asset) = abi.decode(data, (IERC721, IERC20)); require(address(collateral) != address(0), "NFTPair: bad pair"); } - function updateLoanParams(uint256 tokenId, TokenLoanParamsWithOracle memory params) public { + /// @param tokenId The token ID of the loan in question + /// @param params The desired new loan parameters + function updateLoanParams(uint256 tokenId, TokenLoanParamsWithOracle memory params) external { TokenLoan memory loan = tokenLoan[tokenId]; if (loan.status == LOAN_OUTSTANDING) { // The lender can change terms so long as the changes are strictly @@ -207,45 +183,50 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { revert("NFTPair: no collateral"); } tokenLoanParams[tokenId] = params; - emit LogUpdateLoanParams(tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltvBPS); + emit LogUpdateLoanParams(tokenId, params); } - function _requestLoan( - address collateralProvider, + /// @notice It is the caller's responsibility to ensure skimmed tokens get accounted for somehow so they cannot be used twice. + /// @notice It is the caller's responsibility to ensure `provider` consented to the specific transfer. (EIR-721 approval is not good enough). + function _requireCollateral( + address provider, uint256 tokenId, - TokenLoanParamsWithOracle memory params, - address to, bool skim ) private { - // Edge case: valuation can be zero. That effectively gifts the NFT and - // is therefore a bad idea, but does not break the contract. - require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); if (skim) { require(collateral.ownerOf(tokenId) == address(this), "NFTPair: skim failed"); } else { - collateral.transferFrom(collateralProvider, address(this), tokenId); + collateral.transferFrom(provider, address(this), tokenId); } - TokenLoan memory loan; - loan.borrower = to; - loan.status = LOAN_REQUESTED; - tokenLoan[tokenId] = loan; - tokenLoanParams[tokenId] = params; - - emit LogRequestLoan(to, tokenId, params.valuation, params.duration, params.annualInterestBPS, params.ltvBPS); } /// @notice Deposit an NFT as collateral and request a loan against it /// @param tokenId ID of the NFT /// @param to Address to receive the loan, or option to withdraw collateral /// @param params Loan conditions on offer - /// @param skim True if the token has already been transfered + /// @param skim True if the token has already been transferred function requestLoan( uint256 tokenId, TokenLoanParamsWithOracle memory params, address to, bool skim ) public { - _requestLoan(msg.sender, tokenId, params, to, skim); + // Edge case: valuation can be zero. That effectively gifts the NFT and + // is therefore a bad idea, but does not break the contract. + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_INITIAL, "NFTPair: loan exists"); + + loan.borrower = to; + loan.status = LOAN_REQUESTED; + tokenLoan[tokenId] = loan; + tokenLoanParams[tokenId] = params; + + emit LogRequestLoan(to, tokenId, params); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + _requireCollateral(msg.sender, tokenId, skim); } /// @notice Removes `tokenId` as collateral and transfers it to `to`. @@ -258,64 +239,59 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // We are withdrawing collateral that is not in use: require(msg.sender == loan.borrower, "NFTPair: not the borrower"); } else if (loan.status == LOAN_OUTSTANDING) { - // We are seizing collateral towards the lender. The loan has to be - // expired and not paid off, or underwater and not paid off: + // We are seizing collateral as the lender. The loan has to be + // expired and not paid off: require(to == loan.lender || msg.sender == loan.lender, "NFTPair: not the lender"); - if (uint256(loan.startTime) + tokenLoanParams[tokenId].duration > block.timestamp) { - TokenLoanParamsWithOracle memory loanParams = tokenLoanParams[tokenId]; - // No underflow: loan.startTime is only ever set to a block timestamp - // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest( - loanParams.valuation, - uint64(block.timestamp - loan.startTime), - loanParams.annualInterestBPS - ).to128(); - uint256 amount = loanParams.valuation + interest; - (, uint256 rate) = loanParams.oracle.get(address(this), tokenId); - require(rate.mul(loanParams.ltvBPS) / BPS < amount, "NFT is still valued"); + TokenLoanParamsWithOracle memory params = tokenLoanParams[tokenId]; + // If the loan has not expired, but debt exceeds LTV, collateral can + // be seized: + // No overflow: Both summands are smaller than 256 bits + if (uint256(loan.startTime) + params.duration >= block.timestamp) { + // No underflow: loan.startTime is set to a previous timestamp + // Cast is safe: No loan will be active if this overflows + uint256 interest = calculateInterest(params.valuation, uint64(block.timestamp - loan.startTime), params.annualInterestBPS); + uint256 amount = params.valuation + interest; + (bool ok, uint256 rate) = params.oracle.get(address(this), tokenId); + require(ok, "NFTPair: oracle not working"); + // No overflow: amount cannot exceed 129 bits + require(rate.mul(params.ltvBPS) < amount * BPS, "NFTPair: LTV not exceeded"); } + // Else the loan has expired, and the collateral can be taken always } // If there somehow is collateral but no accompanying loan, then anyone // can claim it by first requesting a loan with `skim` set to true, and // then withdrawing. So we might as well allow it here.. delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; collateral.transferFrom(address(this), to, tokenId); emit LogRemoveCollateral(tokenId, to); } - // Assumes the lender has agreed to the loan. + ///@notice Assumes the lender has agreed to the loan. + ///@param borrower Receives the option to repay and get the collateral back + ///@param initialRecipient Receives the initial funds function _lend( address lender, + address borrower, + address initialRecipient, uint256 tokenId, - TokenLoanParamsWithOracle memory accepted, + TokenLoanParamsWithOracle memory params, bool skim - ) internal { - TokenLoan memory loan = tokenLoan[tokenId]; - require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); - TokenLoanParamsWithOracle memory params = tokenLoanParams[tokenId]; - - // Valuation has to be an exact match, everything else must be at least - // as good for the lender as `accepted`. - require( - params.valuation == accepted.valuation && - params.duration <= accepted.duration && - params.annualInterestBPS >= accepted.annualInterestBPS && - params.ltvBPS <= accepted.ltvBPS && - params.oracle == accepted.oracle, - "NFTPair: bad params" - ); - - if (params.oracle != INFTOracle(0)) { - (, uint256 rate) = params.oracle.get(address(this), tokenId); - require(rate.mul(uint256(params.ltvBPS)) / BPS >= params.valuation, "Oracle: price too low."); - } - + ) internal returns (uint256 borrowerShare) { uint256 totalShare = bentoBox.toShare(asset, params.valuation, false); // No overflow: at most 128 + 16 bits (fits in BentoBox) uint256 openFeeShare = (totalShare * OPEN_FEE_BPS) / BPS; uint256 protocolFeeShare = (openFeeShare * PROTOCOL_FEE_BPS) / BPS; + // Protect tokens that are underwater to begin with from being "lent" + // against (and seized immediately): + if (params.oracle != INFTOracle(0)) { + (bool ok, uint256 rate) = params.oracle.get(address(this), tokenId); + require(ok, "NFTPair: oracle not working"); + require(rate.mul(params.ltvBPS) >= params.valuation * BPS, "NFTPair: already underwater"); + } + if (skim) { require( bentoBox.balanceOf(asset, address(this)) >= (totalShare - openFeeShare + protocolFeeShare + feesEarnedShare), @@ -325,29 +301,45 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { bentoBox.transfer(asset, lender, address(this), totalShare - openFeeShare + protocolFeeShare); } // No underflow: follows from OPEN_FEE_BPS <= BPS - uint256 borrowerShare = totalShare - openFeeShare; - bentoBox.transfer(asset, address(this), loan.borrower, borrowerShare); + borrowerShare = totalShare - openFeeShare; + bentoBox.transfer(asset, address(this), initialRecipient, borrowerShare); // No overflow: addends (and result) must fit in BentoBox feesEarnedShare += protocolFeeShare; + TokenLoan memory loan; loan.lender = lender; + loan.borrower = borrower; loan.status = LOAN_OUTSTANDING; loan.startTime = uint64(block.timestamp); // Do not use in 12e10 years.. tokenLoan[tokenId] = loan; - emit LogLend(lender, tokenId); + emit LogLend(lender, borrower, tokenId, params); } /// @notice Lends with the parameters specified by the borrower. /// @param tokenId ID of the token that will function as collateral /// @param accepted Loan parameters as the lender saw them, for security - /// @param skim True if the funds have been transfered to the contract + /// @param skim True if the funds have been Bento-transferred to the contract function lend( uint256 tokenId, TokenLoanParamsWithOracle memory accepted, bool skim ) public { - _lend(msg.sender, tokenId, accepted, skim); + TokenLoan memory loan = tokenLoan[tokenId]; + require(loan.status == LOAN_REQUESTED, "NFTPair: not available"); + TokenLoanParamsWithOracle memory requested = tokenLoanParams[tokenId]; + + // Valuation has to be an exact match, everything else must be at least + // as good for the lender as `accepted`. + require( + requested.valuation == accepted.valuation && + requested.duration <= accepted.duration && + requested.annualInterestBPS >= accepted.annualInterestBPS && + requested.ltvBPS <= accepted.ltvBPS && + requested.oracle == accepted.oracle, + "NFTPair: bad params" + ); + _lend(msg.sender, loan.borrower, loan.borrower, tokenId, requested, skim); } // solhint-disable-next-line func-name-mixedcase @@ -357,7 +349,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // NOTE on signature hashes: the domain separator only guarantees that the // chain ID and master contract are a match, so we explicitly include the - // clone address (and the asset/collateral addresses): + // clone address // keccak256("Lend(address contract,uint256 tokenId,bool anyTokenId,uint128 valuation,uint64 duration,uint16 annualInterestBPS,uint16 ltvBPS,address oracle,uint256 nonce,uint256 deadline)") bytes32 private constant LEND_SIGNATURE_HASH = 0x4bfd5d24664945f4bb81f6061bd624907d74ba338190bdd6aa37f65838a8a533; @@ -370,21 +362,103 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { /// @notice Caller provides collateral; loan can go to a different address. /// @param tokenId ID of the token that will function as collateral /// @param lender Lender, whose BentoBox balance the funds will come from - /// @param recipient Address to receive the loan. + /// @param borrower Receives the funds and the option to repay /// @param params Loan parameters requested, and signed by the lender - /// @param skimCollateral True if the collateral has already been transfered + /// @param skimCollateral True if the collateral has already been transferred /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + /// @param signature (deadline, v, r, s) of signature. (See docs) function requestAndBorrow( uint256 tokenId, address lender, - address recipient, + address borrower, TokenLoanParamsWithOracle memory params, bool skimCollateral, bool anyTokenId, SignatureParams memory signature ) public { + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + _lend(lender, borrower, borrower, tokenId, params, false); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + // (The check is in `_requireSignedLendParams()`) + _requireCollateral(msg.sender, tokenId, skimCollateral); + } + + /// @notice Request and immediately borrow from a pre-committed lender, while buying the collateral in the same transaction. + /// @notice Caller provides extra funds if needed; loan can go to a different address. + /// @param tokenId ID of the token that will function as collateral + /// @param lender Lender, whose BentoBox balance the funds will come from + /// @param borrower Receives the funds (and excess if token is cheaper) + /// @param params Loan parameters requested, and signed by the lender + /// @param anyTokenId Set if lender agreed to any token. Must have tokenId 0 in signature. + /// @param signature (deadline, v, r, s) of signature. (See docs) + /// @param price Price of token (in wei), sent to buyer contract + /// @param buyer INFTBuyer contract that will purchase the token + /// @param skimShortage True if any funds needed in excess of the loan have already been Bento-transfered to the contract + function flashRequestAndBorrow( + uint256 tokenId, + address lender, + address borrower, + TokenLoanParamsWithOracle memory params, + bool anyTokenId, + SignatureParams memory signature, + uint256 price, + INFTBuyer buyer, + bool skimShortage + ) external { + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + // Round up: this is how many Bento-shares it will take to withdraw + // `price` tokens + uint256 priceShare = bentoBox.toShare(asset, price, true); + // Bento-shares received by taking out the loan. They are sent to the + // buyer contract for skimming. + // TODO: Allow Bento-withdrawing instead? + uint256 borrowerShare = _lend(lender, borrower, address(this), tokenId, params, false); + // At this point the contract has `borrowerShare` extra shares. If this + // is too much, then the borrower gets the excess. If this is not + // enough, we either take the rest from msg.sender, or have the amount + // skimmed. + if (borrowerShare > priceShare) { + bentoBox.transfer(asset, address(this), borrower, borrowerShare - priceShare); + } else if (borrowerShare < priceShare) { + if (skimShortage) { + // We have `borrowerShare`, but need `priceShare`: + require(bentoBox.balanceOf(asset, address(this)) >= (priceShare + feesEarnedShare), "NFTPair: skim too much"); + } else { + // We need the difference: + bentoBox.transfer(asset, msg.sender, address(this), priceShare - borrowerShare); + } + } + // The share amount taken will be exactly `priceShare`, and the token + // amount will be exactly `price`. If we passed `priceShare` instead, + // the token amount given could be different. + bentoBox.withdraw(asset, address(this), address(buyer), price, 0); + + // External call is safe: At this point, the state of the contract is + // unusual only in that it has issued a loan against the token that has + // not been delivered yet. Any interaction that does not involve this + // loan/token is no different from outside this call. Taking out a new + // loan is not possible. Repaying the loan is, but requires that: + // a) the buyer either is, or has the token sent to, the borrower; + // b) the token is sent to the contract first -- `_repayBefore()` will + // try to transfer it away. + // By (b) in particular, the buyer contract cannot exploit this + // situation. + buyer.buy(asset, price, collateral, tokenId, address(this)); + require(collateral.ownerOf(tokenId) == address(this), "NFTPair: buyer failed"); + } + + function _requireSignedLendParams( + address lender, + uint256 tokenId, + TokenLoanParamsWithOracle memory params, + bool anyTokenId, + SignatureParams memory signature + ) private { if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClubWithOracle(lender).willLend(tokenId, params), "NFTPair: LendingClub does not like you"); + require(ILendingClubWithOracle(lender).willLend(tokenId, params), "NFTPair: LendingClub refused"); } else { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; @@ -405,23 +479,17 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { ); require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == lender, "NFTPair: signature invalid"); } - _requestLoan(msg.sender, tokenId, params, recipient, skimCollateral); - _lend(lender, tokenId, params, false); + + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + tokenLoanParams[tokenId] = params; } - /// @notice Take collateral from a pre-commited borrower and lend against it - /// @notice Collateral must come from the borrower, not a third party. - /// @param tokenId ID of the token that will function as collateral - /// @param borrower Address that provides collateral and receives the loan - /// @param params Loan terms offered, and signed by the borrower - /// @param skimFunds True if the funds have been transfered to the contract - function takeCollateralAndLend( - uint256 tokenId, + function _requireSignedBorrowParams( address borrower, + uint256 tokenId, TokenLoanParamsWithOracle memory params, - bool skimFunds, SignatureParams memory signature - ) public { + ) private { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[borrower]++; bytes32 dataHash = keccak256( @@ -439,27 +507,56 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { ) ); require(ecrecover(_getDigest(dataHash), signature.v, signature.r, signature.s) == borrower, "NFTPair: signature invalid"); - _requestLoan(borrower, tokenId, params, borrower, false); - _lend(msg.sender, tokenId, params, skimFunds); + require(tokenLoan[tokenId].status == LOAN_INITIAL, "NFTPair: loan exists"); + tokenLoanParams[tokenId] = params; } - /// Approximates continuous compounding. Uses Horner's method to evaluate - /// the truncated Maclaurin series for exp - 1, accumulating rounding - /// errors along the way. The following is always guaranteed: - /// - /// principal * time * apr <= result <= principal * (e^(time * apr) - 1), - /// - /// where time = t/YEAR, up to at most the rounding error obtained in - /// calculating linear interest. - /// - /// If the theoretical result that we are approximating (the rightmost part - /// of the above inquality) fits in 128 bits, then the function is - /// guaranteed not to revert (unless n > 250, which is way too high). - /// If even the linear interest (leftmost part of the inequality) does not - /// the function will revert. - /// Otherwise, the function may revert, return a reasonable result, or - /// return a very inaccurate result. Even then the above inequality is - /// respected. + /// @notice Take collateral from a pre-commited borrower and lend against it + /// @notice Collateral must come from the borrower, not a third party. + /// @param tokenId ID of the token that will function as collateral + /// @param borrower Address that provides collateral and receives the loan + /// @param params Loan terms offered, and signed by the borrower + /// @param skimFunds True if the funds have been Bento-transferred to the contract + /// @param signature (deadline, v, r, s) of signature. (See docs) + function takeCollateralAndLend( + uint256 tokenId, + address borrower, + TokenLoanParamsWithOracle memory params, + bool skimFunds, + SignatureParams memory signature + ) public { + _requireSignedBorrowParams(borrower, tokenId, params, signature); + _lend(msg.sender, borrower, borrower, tokenId, params, skimFunds); + // Skimming is safe: + // - This method both requires loan state to be LOAN_INITIAL and sets + // it to something else. Every other use of _requireCollateral must + // uphold this same requirement; see to it. + // (The check is in `_requireSignedBorrowParams()`) + // Taking collateral from someone other than msg.sender is safe: the + // borrower signed a message giving permission. + _requireCollateral(borrower, tokenId, false); + } + + // Approximates continuous compounding. Uses Horner's method to evaluate + // the truncated Maclaurin series for exp - 1, accumulating rounding + // errors along the way. The following is always guaranteed: + // + // principal * time * apr <= result <= principal * (e^(time * apr) - 1), + // + // where time = t/YEAR, up to at most the rounding error obtained in + // calculating linear interest. + // + // If the theoretical result that we are approximating (the rightmost part + // of the above inquality) fits in 128 bits, then the function is + // guaranteed not to revert (unless n > 250, which is way too high). + // If even the linear interest (leftmost part of the inequality) does not + // the function will revert. + // Otherwise, the function may revert, return a reasonable result, or + // return a very inaccurate result. Even then the above inequality is + // respected. + /// @param principal Amount owed in wei + /// @param t Duration in seconds + /// @param aprBPS Annual rate in basis points (1/10_000) function calculateInterest( uint256 principal, uint64 t, @@ -477,7 +574,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // // which approaches, but never exceeds the "theoretical" result, // - // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 + // M := principal * [ exp (t * aprBPS / YEAR_BPS) - 1 ] // // as n goes to infinity. We use the fact that // @@ -520,17 +617,27 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } if (interest >= 2**128) { - revert(); + revert("NFTPair: Interest unpayable"); } } - function repay(uint256 tokenId, bool skim) public returns (uint256 amount) { + function _repayBefore(uint256 tokenId, address to) + private + returns ( + uint256 totalShare, + uint256 totalAmount, + uint256 feeShare, + address lender + ) + { TokenLoan memory loan = tokenLoan[tokenId]; require(loan.status == LOAN_OUTSTANDING, "NFTPair: no loan"); + require(msg.sender == loan.borrower || to == loan.borrower, "NFTPair: not borrower"); + TokenLoanParamsWithOracle memory loanParams = tokenLoanParams[tokenId]; require( // Addition is safe: both summands are smaller than 256 bits - uint256(loan.startTime) + loanParams.duration > block.timestamp, + uint256(loan.startTime) + loanParams.duration >= block.timestamp, "NFTPair: loan expired" ); @@ -538,33 +645,129 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // No underflow: loan.startTime is only ever set to a block timestamp // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS).to128(); + uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); + // No overflow: multiplicands fit in 128 and 16 bits uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; - amount = principal + interest; + // No overflon: both terms are 128 bits + totalAmount = principal + interest; + + totalShare = bentoBox.toShare(asset, totalAmount, false); + feeShare = bentoBox.toShare(asset, fee, false); + lender = loan.lender; - uint256 totalShare = bentoBox.toShare(asset, amount, false); - uint256 feeShare = bentoBox.toShare(asset, fee, false); + delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; + + collateral.transferFrom(address(this), to, tokenId); + } - address from; + function _repayAfter( + address lender, + uint256 totalShare, + uint256 feeShare, + uint256 tokenId, + bool skim + ) private { + // No overflow: `totalShare - feeShare` is 90% of `totalShare`, and + // if that exceeds 128 bits the BentoBox transfer will revert. It + // follows that `totalShare` fits in 129 bits, and `feesEarnedShare` + // fits in 128 as it represents a BentoBox balance. + // Skimming is safe: the amount gets transferred to the lender later, + // and therefore cannot be skimmed twice. if (skim) { require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); - from = address(this); - // No overflow: result fits in BentoBox } else { - bentoBox.transfer(asset, msg.sender, address(this), feeShare); - from = msg.sender; + bentoBox.transfer(asset, msg.sender, address(this), totalShare); } - // No underflow: PROTOCOL_FEE_BPS < BPS by construction. + // No overflow: result fits in BentoBox feesEarnedShare += feeShare; - delete tokenLoan[tokenId]; + // No underflow: `feeShare` is 10% of part of `totalShare` + bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); + emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } - bentoBox.transfer(asset, from, loan.lender, totalShare - feeShare); - collateral.transferFrom(address(this), loan.borrower, tokenId); + /// @notice Repay a loan in full + /// @param tokenId Token ID of the loan in question. + /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. + /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. + function repay( + uint256 tokenId, + address to, + bool skim + ) external { + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to); + _repayAfter(lender, totalShare, feeShare, tokenId, skim); + } - emit LogRepay(from, tokenId); + /// @notice Repay a loan in full, by selling the token in the same transaction. Must be the borrower. + /// @param tokenId Token ID of the loan in question. + /// @param price Sale price of the token, in wei + /// @param seller INFTSeller contract that will perform the sale + /// @param excessRecipient Receives any funds left over after repaying, if any + /// @param skimShortage True if any extra funds required have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. + function flashRepay( + uint256 tokenId, + uint256 price, + INFTSeller seller, + address excessRecipient, + bool skimShortage + ) external { + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller)); + + // External call is safe: At this point the loan is already gone, the + // seller has the token, and an amount must be paid via skimming or the + // entire transaction reverts. + // Other than being owed the money, the contract is in a valid state, + // and once payment is received it is "accounted for" by being sent + // away (in `_repayAfter()`), so that it cannot be reused for skimming. + // Relying on return value is safe: if the amount reported is too high, + // then either `_repayAfter()` will fail, or the funds were sitting in + // the contract's BentoBox balance unaccounted for, and could be freely + // skimmed for another purpose anyway. + uint256 priceShare = seller.sell(collateral, tokenId, asset, price, address(this)); + if (priceShare < totalShare) { + // No overflow: `totalShare` fits or `_repayAfter()` reverts. See + // comments there for proof. + // If we are skimming, then we defer the check to `_repayAfter()`, + // which checks that the full amount (`totalShare`) has been sent. + if (!skimShortage) { + bentoBox.transfer(asset, msg.sender, address(this), totalShare - priceShare); + } + } else if (priceShare > totalShare) { + bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); + } + _repayAfter(lender, totalShare, feeShare, tokenId, true); } - uint8 internal constant ACTION_REPAY = 2; + /// @notice Withdraws the fees accumulated. + function withdrawFees() public { + address to = masterContract.feeTo(); + + uint256 _share = feesEarnedShare; + if (_share > 0) { + bentoBox.transfer(asset, address(this), to, _share); + feesEarnedShare = 0; + } + + emit LogWithdrawFees(to, _share); + } + + /// @notice Sets the beneficiary of fees accrued in liquidations. + /// MasterContract Only Admin function. + /// @param newFeeTo The address of the receiver. + function setFeeTo(address newFeeTo) public onlyOwner { + feeTo = newFeeTo; + emit LogFeeTo(newFeeTo); + } + + //// Cook actions + + // Information only + uint8 internal constant ACTION_GET_AMOUNT_DUE = 1; + uint8 internal constant ACTION_GET_SHARES_DUE = 2; + + // End up owing collateral + uint8 internal constant ACTION_REPAY = 3; uint8 internal constant ACTION_REMOVE_COLLATERAL = 4; uint8 internal constant ACTION_REQUEST_LOAN = 12; @@ -648,23 +851,54 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { return (returnData, returnValues); } - /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. - /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). - /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. - /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. - /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. - /// @return value1 May contain the first positioned return value of the last executed action (if applicable). - /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). - function cook( + // (For the cook action) + function _getAmountDue(uint256 tokenId) private view returns (uint256) { + TokenLoanParamsWithOracle memory params = tokenLoanParams[tokenId]; + // No underflow: startTime is always set to some block timestamp + uint256 principal = params.valuation; + uint256 interest = calculateInterest(principal, uint64(block.timestamp - tokenLoan[tokenId].startTime), params.annualInterestBPS); + // No overflow: both terms are 128 bits + return principal + interest; + } + + function _cook( uint8[] calldata actions, uint256[] calldata values, - bytes[] calldata datas - ) external payable returns (uint256 value1, uint256 value2) { - for (uint256 i = 0; i < actions.length; i++) { + bytes[] calldata datas, + uint256 i, + uint256[2] memory result + ) private { + for (; i < actions.length; i++) { uint8 action = actions[i]; - if (action == ACTION_REPAY) { - (uint256 tokenId, bool skim) = abi.decode(datas[i], (uint256, bool)); - repay(tokenId, skim); + if (action == ACTION_GET_AMOUNT_DUE) { + uint256 tokenId = abi.decode(datas[i], (uint256)); + result[0] = _getAmountDue(tokenId); + } else if (action == ACTION_GET_SHARES_DUE) { + uint256 tokenId = abi.decode(datas[i], (uint256)); + result[1] = _getAmountDue(tokenId); + result[0] = bentoBox.toShare(asset, result[1], false); + } else if (action == ACTION_REPAY) { + uint256 tokenId; + uint256 totalShare; + uint256 feeShare; + address lender; + bool skim; + { + address to; + // No skimming, but it can sill be done + (tokenId, to, skim) = abi.decode(datas[i], (uint256, address, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to); + // Delaying asset collection until after the rest of the + // cook is safe: after checking.. - `feesEarnedShare` is + // updated after the check - The rest (`totalShare - + // feeShare`) is transferred away It is therefore not + // possible to skim the same amount twice. + // (Reusing `i` slot for stack depth reasons) + } + result[0] = totalShare; + _cook(actions, values, datas, ++i, result); + _repayAfter(lender, totalShare, feeShare, tokenId, skim); + return; } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); removeCollateral(tokenId, to); @@ -687,34 +921,55 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { ); bentoBox.setMasterContractApproval(user, _masterContract, approved, v, r, s); } else if (action == ACTION_BENTO_DEPOSIT) { - (value1, value2) = _bentoDeposit(datas[i], values[i], value1, value2); + (result[0], result[1]) = _bentoDeposit(datas[i], values[i], result[0], result[1]); } else if (action == ACTION_BENTO_WITHDRAW) { - (value1, value2) = _bentoWithdraw(datas[i], value1, value2); + (result[0], result[1]) = _bentoWithdraw(datas[i], result[0], result[1]); } else if (action == ACTION_BENTO_TRANSFER) { (IERC20 token, address to, int256 share) = abi.decode(datas[i], (IERC20, address, int256)); - bentoBox.transfer(token, msg.sender, to, _num(share, value1, value2)); + bentoBox.transfer(token, msg.sender, to, _num(share, result[0], result[1])); } else if (action == ACTION_BENTO_TRANSFER_MULTIPLE) { (IERC20 token, address[] memory tos, uint256[] memory shares) = abi.decode(datas[i], (IERC20, address[], uint256[])); bentoBox.transferMultiple(token, msg.sender, tos, shares); } else if (action == ACTION_CALL) { - (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2); + (bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], result[0], result[1]); if (returnValues == 1) { - (value1) = abi.decode(returnData, (uint256)); + (result[0]) = abi.decode(returnData, (uint256)); } else if (returnValues == 2) { - (value1, value2) = abi.decode(returnData, (uint256, uint256)); + (result[0], result[1]) = abi.decode(returnData, (uint256, uint256)); } } else if (action == ACTION_REQUEST_AND_BORROW) { - ( - uint256 tokenId, - address lender, - address recipient, - TokenLoanParamsWithOracle memory params, - bool skimCollateral, - bool anyTokenId, - SignatureParams memory signature - ) = abi.decode(datas[i], (uint256, address, address, TokenLoanParamsWithOracle, bool, bool, SignatureParams)); - requestAndBorrow(tokenId, lender, recipient, params, skimCollateral, anyTokenId, signature); + bool skimCollateral; + uint256 tokenId; + { + address lender; + address borrower; + TokenLoanParamsWithOracle memory params; + bool anyTokenId; + SignatureParams memory signature; + (tokenId, lender, borrower, params, skimCollateral, anyTokenId, signature) = abi.decode( + datas[i], + (uint256, address, address, TokenLoanParamsWithOracle, bool, bool, SignatureParams) + ); + _requireSignedLendParams(lender, tokenId, params, anyTokenId, signature); + _lend(lender, borrower, borrower, tokenId, params, false); + } + _cook(actions, values, datas, ++i, result); + // Skimming is safe: + // - This call both requires loan state to be LOAN_INITIAL and + // sets it to something else. Every other use of + // `_requireCollateral()` must uphold that same requirement; + // see to it. + // Delaying until after the rest of the cook is safe: + // - If the rest of the cook _also_ takes this collateral + // somehow -- either via skimming, or via just having it + // transferred in -- then it did so by opening a loan. But + // that is only possible if this one (that we are collecting + // the collateral for) got repaid in the mean time, which is + // a silly thing to do, but otherwise legitimate and not an + // exploit. + _requireCollateral(msg.sender, tokenId, skimCollateral); + return; } else if (action == ACTION_TAKE_COLLATERAL_AND_LEND) { ( uint256 tokenId, @@ -728,24 +983,20 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } } - /// @notice Withdraws the fees accumulated. - function withdrawFees() public { - address to = masterContract.feeTo(); - - uint256 _share = feesEarnedShare; - if (_share > 0) { - bentoBox.transfer(asset, address(this), to, _share); - feesEarnedShare = 0; - } - - emit LogWithdrawFees(to, _share); - } - - /// @notice Sets the beneficiary of fees accrued in liquidations. - /// MasterContract Only Admin function. - /// @param newFeeTo The address of the receiver. - function setFeeTo(address newFeeTo) public onlyOwner { - feeTo = newFeeTo; - emit LogFeeTo(newFeeTo); + /// @notice Executes a set of actions and allows composability (contract calls) to other contracts. + /// @param actions An array with a sequence of actions to execute (see ACTION_ declarations). + /// @param values A one-to-one mapped array to `actions`. ETH amounts to send along with the actions. + /// Only applicable to `ACTION_CALL`, `ACTION_BENTO_DEPOSIT`. + /// @param datas A one-to-one mapped array to `actions`. Contains abi encoded data of function arguments. + /// @return value1 May contain the first positioned return value of the last executed action (if applicable). + /// @return value2 May contain the second positioned return value of the last executed action which returns 2 values (if applicable). + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2) { + uint256[2] memory result; + _cook(actions, values, datas, 0, result); + return (result[0], result[1]); } } diff --git a/contracts/interfaces/ILendingClubWithOracle.sol b/contracts/interfaces/ILendingClubWithOracle.sol new file mode 100644 index 00000000..73168ba5 --- /dev/null +++ b/contracts/interfaces/ILendingClubWithOracle.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.12 <0.9.0; +pragma experimental ABIEncoderV2; + +import "./INFTPairWithOracle.sol"; + +interface ILendingClubWithOracle { + // Per token settings. + function willLend(uint256 tokenId, TokenLoanParamsWithOracle memory params) + external + view + returns (bool); + + function lendingConditions(address nftPair, uint256 tokenId) + external + view + returns (TokenLoanParamsWithOracle memory); +} + diff --git a/contracts/interfaces/INFTPair.sol b/contracts/interfaces/INFTPair.sol index ff26e75e..782fbbd2 100644 --- a/contracts/interfaces/INFTPair.sol +++ b/contracts/interfaces/INFTPair.sol @@ -5,6 +5,7 @@ pragma solidity >=0.6.12 <0.9.0; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./IERC721.sol"; +import "./SignatureParams.sol"; interface INFTPair { function collateral() external view returns (IERC721); @@ -24,10 +25,3 @@ struct TokenLoanParams { uint16 annualInterestBPS; // Variable cost of taking out the loan } -struct SignatureParams { - uint256 deadline; - uint8 v; - bytes32 r; - bytes32 s; -} - diff --git a/contracts/interfaces/INFTPairWithOracle.sol b/contracts/interfaces/INFTPairWithOracle.sol new file mode 100644 index 00000000..85e208ba --- /dev/null +++ b/contracts/interfaces/INFTPairWithOracle.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.12 <0.9.0; + +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; +import "./IERC721.sol"; +import "./INFTOracle.sol"; +import "./SignatureParams.sol"; + +// TODO: Add more methods for LendingClubWitOracle integration.. +interface INFTPairWithOracle { + function collateral() external view returns (IERC721); + + function asset() external view returns (IERC20); + + function masterContract() external view returns (address); + + function bentoBox() external view returns (IBentoBoxV1); + + function removeCollateral(uint256 tokenId, address to) external; +} + +struct TokenLoanParamsWithOracle { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 annualInterestBPS; // Variable cost of taking out the loan + uint16 ltvBPS; // Required to avoid liquidation + INFTOracle oracle; // Oracle used for price +} + diff --git a/contracts/interfaces/SignatureParams.sol b/contracts/interfaces/SignatureParams.sol new file mode 100644 index 00000000..fb1c93e3 --- /dev/null +++ b/contracts/interfaces/SignatureParams.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12 <0.9.0; + +struct SignatureParams { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; +} + From 635dc515fdc33a8c1f7f4ea77c1fb9912565871f Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Mon, 6 Jun 2022 05:50:35 +0200 Subject: [PATCH 096/107] (Cache some BentoBox calculations) --- contracts/NFTPair.sol | 6 ++++-- contracts/NFTPairWithOracle.sol | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 6c60ec00..d2f959ce 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -622,8 +622,10 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // No overflon: both terms are 128 bits totalAmount = principal + interest; - totalShare = bentoBox.toShare(asset, totalAmount, false); - feeShare = bentoBox.toShare(asset, fee, false); + Rebase memory bentoBoxTotals = bentoBox.totals(asset); + totalShare = bentoBoxTotals.toBase(totalAmount, false); + feeShare = bentoBoxTotals.toBase(fee, false); + lender = loan.lender; delete tokenLoan[tokenId]; diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 13ddeb60..00aca3fb 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -651,8 +651,10 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // No overflon: both terms are 128 bits totalAmount = principal + interest; - totalShare = bentoBox.toShare(asset, totalAmount, false); - feeShare = bentoBox.toShare(asset, fee, false); + Rebase memory bentoBoxTotals = bentoBox.totals(asset); + totalShare = bentoBoxTotals.toBase(totalAmount, false); + feeShare = bentoBoxTotals.toBase(fee, false); + lender = loan.lender; delete tokenLoan[tokenId]; From a71a60f97e7280459b6e1883e2930cf402f33f05 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 7 Jun 2022 03:54:54 +0200 Subject: [PATCH 097/107] Add partial repayments --- contracts/NFTPair.sol | 65 ++++++++++++++++-------- contracts/NFTPairWithOracle.sol | 65 ++++++++++++++++-------- test/NFTPair.test.ts | 89 ++++++++++++++++++++++++++------- 3 files changed, 158 insertions(+), 61 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index d2f959ce..6acb427e 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -225,6 +225,13 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { _requireCollateral(msg.sender, tokenId, skim); } + /// @dev Assumes all checks have been done + function _finalizeLoan(uint256 tokenId, address collateralTo) private { + delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; + collateral.transferFrom(address(this), collateralTo, tokenId); + } + /// @notice Removes `tokenId` as collateral and transfers it to `to`. /// @notice This destroys the loan. /// @param tokenId The token @@ -247,9 +254,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // If there somehow is collateral but no accompanying loan, then anyone // can claim it by first requesting a loan with `skim` set to true, and // then withdrawing. So we might as well allow it here.. - delete tokenLoan[tokenId]; - delete tokenLoanParams[tokenId]; - collateral.transferFrom(address(this), to, tokenId); + _finalizeLoan(tokenId, to); emit LogRemoveCollateral(tokenId, to); } @@ -592,7 +597,12 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } } - function _repayBefore(uint256 tokenId, address to) + function _repayBefore( + uint256 tokenId, + address to, + uint256 partBPS, + bool skim + ) private returns ( uint256 totalShare, @@ -612,33 +622,42 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { "NFTPair: loan expired" ); - uint128 principal = loanParams.valuation; + uint256 principal = loanParams.valuation; + if (partBPS < BPS) { + // Math is safe: principal fits in 128 bits + principal = (principal * partBPS) / BPS; + // Math and cast are safe: principal is less than valuation + loanParams.valuation = uint128(loanParams.valuation - principal); + tokenLoanParams[tokenId] = loanParams; + emit LogUpdateLoanParams(tokenId, loanParams); + } else { + // Not following checks-effects-interaction: we are already not + // doing that by splitting `repay()` like this; we'll have to trust + // the collateral contract if we are to support flash repayments. + _finalizeLoan(tokenId, to); + emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe (principal): is LTE loan.valuation // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); + uint256 interest = calculateInterest(uint128(principal), uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); // No overflow: multiplicands fit in 128 and 16 bits uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; // No overflon: both terms are 128 bits totalAmount = principal + interest; Rebase memory bentoBoxTotals = bentoBox.totals(asset); + totalShare = bentoBoxTotals.toBase(totalAmount, false); feeShare = bentoBoxTotals.toBase(fee, false); - lender = loan.lender; - - delete tokenLoan[tokenId]; - delete tokenLoanParams[tokenId]; - - collateral.transferFrom(address(this), to, tokenId); } function _repayAfter( address lender, uint256 totalShare, uint256 feeShare, - uint256 tokenId, bool skim ) private { // No overflow: `totalShare - feeShare` is 90% of `totalShare`, and @@ -656,20 +675,21 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { feesEarnedShare += feeShare; // No underflow: `feeShare` is 10% of part of `totalShare` bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); - emit LogRepay(skim ? address(this) : msg.sender, tokenId); } - /// @notice Repay a loan in full + /// @notice Repay a loan in part or in full /// @param tokenId Token ID of the loan in question. /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. + /// @param partBPS Part of the loan to repay, in BPS. (Saturates at 10k). /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function repay( uint256 tokenId, address to, + uint16 partBPS, bool skim ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to); - _repayAfter(lender, totalShare, feeShare, tokenId, skim); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to, partBPS, skim); + _repayAfter(lender, totalShare, feeShare, skim); } /// @notice Repay a loan in full, by selling the token in the same transaction. Must be the borrower. @@ -685,7 +705,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address excessRecipient, bool skimShortage ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller)); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller), BPS, false); // External call is safe: At this point the loan is already gone, the // seller has the token, and an amount must be paid via skimming or the @@ -709,7 +729,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } else if (priceShare > totalShare) { bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); } - _repayAfter(lender, totalShare, feeShare, tokenId, true); + _repayAfter(lender, totalShare, feeShare, true); } /// @notice Withdraws the fees accumulated. @@ -855,12 +875,13 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 totalShare; uint256 feeShare; address lender; + uint16 partBPS; bool skim; { address to; // No skimming, but it can sill be done - (tokenId, to, skim) = abi.decode(datas[i], (uint256, address, bool)); - (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to); + (tokenId, to, partBPS, skim) = abi.decode(datas[i], (uint256, address, uint16, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to, partBPS, skim); // Delaying asset collection until after the rest of the // cook is safe: after checking.. - `feesEarnedShare` is // updated after the check - The rest (`totalShare - @@ -870,7 +891,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } result[0] = totalShare; _cook(actions, values, datas, ++i, result); - _repayAfter(lender, totalShare, feeShare, tokenId, skim); + _repayAfter(lender, totalShare, feeShare, skim); return; } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 00aca3fb..3c463d00 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -229,6 +229,13 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { _requireCollateral(msg.sender, tokenId, skim); } + /// @dev Assumes all checks have been done + function _finalizeLoan(uint256 tokenId, address collateralTo) private { + delete tokenLoan[tokenId]; + delete tokenLoanParams[tokenId]; + collateral.transferFrom(address(this), collateralTo, tokenId); + } + /// @notice Removes `tokenId` as collateral and transfers it to `to`. /// @notice This destroys the loan. /// @param tokenId The token @@ -262,9 +269,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // If there somehow is collateral but no accompanying loan, then anyone // can claim it by first requesting a loan with `skim` set to true, and // then withdrawing. So we might as well allow it here.. - delete tokenLoan[tokenId]; - delete tokenLoanParams[tokenId]; - collateral.transferFrom(address(this), to, tokenId); + _finalizeLoan(tokenId, to); emit LogRemoveCollateral(tokenId, to); } @@ -621,7 +626,12 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } } - function _repayBefore(uint256 tokenId, address to) + function _repayBefore( + uint256 tokenId, + address to, + uint256 partBPS, + bool skim + ) private returns ( uint256 totalShare, @@ -641,33 +651,42 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { "NFTPair: loan expired" ); - uint128 principal = loanParams.valuation; + uint256 principal = loanParams.valuation; + if (partBPS < BPS) { + // Math is safe: principal fits in 128 bits + principal = (principal * partBPS) / BPS; + // Math and cast are safe: principal is less than valuation + loanParams.valuation = uint128(loanParams.valuation - principal); + tokenLoanParams[tokenId] = loanParams; + emit LogUpdateLoanParams(tokenId, loanParams); + } else { + // Not following checks-effects-interaction: we are already not + // doing that by splitting `repay()` like this; we'll have to trust + // the collateral contract if we are to support flash repayments. + _finalizeLoan(tokenId, to); + emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } // No underflow: loan.startTime is only ever set to a block timestamp + // Cast is safe (principal): is LTE loan.valuation // Cast is safe: if this overflows, then all loans have expired anyway - uint256 interest = calculateInterest(principal, uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); + uint256 interest = calculateInterest(uint128(principal), uint64(block.timestamp - loan.startTime), loanParams.annualInterestBPS); // No overflow: multiplicands fit in 128 and 16 bits uint256 fee = (interest * PROTOCOL_FEE_BPS) / BPS; // No overflon: both terms are 128 bits totalAmount = principal + interest; Rebase memory bentoBoxTotals = bentoBox.totals(asset); + totalShare = bentoBoxTotals.toBase(totalAmount, false); feeShare = bentoBoxTotals.toBase(fee, false); - lender = loan.lender; - - delete tokenLoan[tokenId]; - delete tokenLoanParams[tokenId]; - - collateral.transferFrom(address(this), to, tokenId); } function _repayAfter( address lender, uint256 totalShare, uint256 feeShare, - uint256 tokenId, bool skim ) private { // No overflow: `totalShare - feeShare` is 90% of `totalShare`, and @@ -685,20 +704,21 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { feesEarnedShare += feeShare; // No underflow: `feeShare` is 10% of part of `totalShare` bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); - emit LogRepay(skim ? address(this) : msg.sender, tokenId); } - /// @notice Repay a loan in full + /// @notice Repay a loan in part or in full /// @param tokenId Token ID of the loan in question. /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. + /// @param partBPS Part of the loan to repay, in BPS. (Saturates at 10k). /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function repay( uint256 tokenId, address to, + uint16 partBPS, bool skim ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to); - _repayAfter(lender, totalShare, feeShare, tokenId, skim); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to, partBPS, skim); + _repayAfter(lender, totalShare, feeShare, skim); } /// @notice Repay a loan in full, by selling the token in the same transaction. Must be the borrower. @@ -714,7 +734,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { address excessRecipient, bool skimShortage ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller)); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller), BPS, false); // External call is safe: At this point the loan is already gone, the // seller has the token, and an amount must be paid via skimming or the @@ -738,7 +758,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } else if (priceShare > totalShare) { bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); } - _repayAfter(lender, totalShare, feeShare, tokenId, true); + _repayAfter(lender, totalShare, feeShare, true); } /// @notice Withdraws the fees accumulated. @@ -884,12 +904,13 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 totalShare; uint256 feeShare; address lender; + uint16 partBPS; bool skim; { address to; // No skimming, but it can sill be done - (tokenId, to, skim) = abi.decode(datas[i], (uint256, address, bool)); - (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to); + (tokenId, to, partBPS, skim) = abi.decode(datas[i], (uint256, address, uint16, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to, partBPS, skim); // Delaying asset collection until after the rest of the // cook is safe: after checking.. - `feesEarnedShare` is // updated after the check - The rest (`totalShare - @@ -899,7 +920,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } result[0] = totalShare; _cook(actions, values, datas, ++i, result); - _repayAfter(lender, totalShare, feeShare, tokenId, skim); + _repayAfter(lender, totalShare, feeShare, skim); return; } else if (action == ACTION_REMOVE_COLLATERAL) { (uint256 tokenId, address to) = abi.decode(datas[i], (uint256, address)); diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index 44f75cca..f5cfcd73 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -708,13 +708,13 @@ describe("NFT Pair", async () => { // happens: const YEAR_BPS = YEAR * 10_000; const COMPOUND_TERMS = 6; - const getMaxRepayShare = (time, params_) => { + const getMaxRepayShare = (time, params_, part = 10_000) => { // We mimic what the contract does, but without rounding errors in the // approximation, so the upper bound should be strict. // 1. Calculate exact amount owed; round it down, like the contract does. // 2. Convert that to Bento shares (still hardcoded at 9/20); rounding up const x = BigRational.from(time * params_.annualInterestBPS).div(YEAR_BPS); - return expApprox(x, COMPOUND_TERMS).mul(params_.valuation).floor().mul(9).add(19).div(20); + return expApprox(x, COMPOUND_TERMS).mul(params_.valuation).mul(part).div(10_000).floor().mul(9).add(19).div(20); }; before(async () => { @@ -742,7 +742,7 @@ describe("NFT Pair", async () => { // Two Bento transfers: payment to the lender, fee to the contract await advanceNextTime(DAY); - await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, false)) + await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, 10_000, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -784,7 +784,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await advanceNextTime(DAY); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, false)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, false)) .to.emit(pair, "LogRepay") .withArgs(carol.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -835,7 +835,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, true)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, true)) .to.emit(pair, "LogRepay") .withArgs(pair.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -891,7 +891,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, alice.address, false])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, alice.address, 10_000, false])); await expect(pair.connect(carol).cook(actions, values, datas)) .to.emit(pair, "LogRepay") @@ -956,7 +956,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, alice.address, true])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, alice.address, 10_000, true])); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); await expect(pair.connect(carol).cook(actions, values, datas)) @@ -1021,7 +1021,7 @@ describe("NFT Pair", async () => { const inFive = await advanceNextTime(fiveYears); - await expect(pair.connect(alice).repay(apeIds.aliceTwo, alice.address, false)) + await expect(pair.connect(alice).repay(apeIds.aliceTwo, alice.address, 10_000, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceTwo) .to.emit(apes, "Transfer") @@ -1059,12 +1059,12 @@ describe("NFT Pair", async () => { it("Should refuse repayments on expired loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); - await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, false)).to.be.revertedWith("NFTPair: loan expired"); + await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, 10_000, false)).to.be.revertedWith("NFTPair: loan expired"); }); it("Should refuse repayments on nonexistent loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); - await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, false)).to.be.revertedWith("NFTPair: no loan"); + await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, 10_000, false)).to.be.revertedWith("NFTPair: no loan"); }); it("Should refuse to skim too much", async () => { @@ -1079,7 +1079,62 @@ describe("NFT Pair", async () => { await bentoBox.connect(bob).transfer(guineas.address, bob.address, pair.address, notEnoughShare); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, true)).to.be.revertedWith("NFTPair: skim too much"); + await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, true)).to.be.revertedWith("NFTPair: skim too much"); + }); + + it("Should allow partial repayments", async () => { + const getBalances = async () => ({ + alice: await bentoBox.balanceOf(guineas.address, alice.address), + bob: await bentoBox.balanceOf(guineas.address, bob.address), + pair: await bentoBox.balanceOf(guineas.address, pair.address), + feeTracker: await pair.feesEarnedShare(), + }); + const t0 = await getBalances(); + + const part = 1337; // Parts out of 10k that get paid off + + const afterParams = { + ...params, + // We round down when calculating how much to repay, so round up here: + valuation: params.valuation + .mul(10_000 - part) + .add(9999) + .div(10_000), + }; + + // Two Bento transfers: payment to the lender, fee to the contract + await advanceNextTime(DAY); + await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, part, false)) + .to.emit(pair, "LogUpdateLoanParams") + .withArgs(apeIds.aliceOne, paramsArray(afterParams)) + .to.emit(bentoBox, "LogTransfer") + .to.emit(bentoBox, "LogTransfer"); + + // The NFT remains with the contract + expect(await apes.ownerOf(apeIds.aliceOne)).to.equal(pair.address); + expect((await pair.tokenLoanParams(apeIds.aliceOne)).valuation).to.equal(afterParams.valuation); + + const t1 = await getBalances(); + const maxRepayShare = getMaxRepayShare(DAY, params, part); + const partValuationShare = valuationShare.mul(part).div(10_000); + const linearInterest = partValuationShare.mul(params.annualInterestBPS).mul(DAY).div(YEAR_BPS); + + const paid = t0.alice.sub(t1.alice); + expect(paid).to.be.gte(partValuationShare.add(linearInterest)); + expect(paid).to.be.lte(maxRepayShare); + + // The difference is rounding errors only, so should be very small: + const paidError = maxRepayShare.sub(paid); + expect(paidError.mul(1_000_000_000)).to.be.lt(paid); + + // The fee is hardcoded at 10% of the interest + const fee = t1.feeTracker.sub(t0.feeTracker); + expect(fee.mul(10)).to.be.gte(linearInterest); + expect(fee.mul(10)).to.be.lte(paid.sub(partValuationShare)); + expect(t1.pair.sub(t0.pair)).to.equal(fee); + + const received = t1.bob.sub(t0.bob); + expect(received.add(fee)).to.equal(paid); }); }); @@ -1265,7 +1320,7 @@ describe("NFT Pair", async () => { await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.emit(pair, "LogLend"); // Carol repays the loan to get the token back: - await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, 10_000, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.carolOne)).to.equal(carol.address); // It fails now (because the nonce is no longer a match): @@ -1397,7 +1452,7 @@ describe("NFT Pair", async () => { ); // Bob repays the loan to get the token back: - await expect(pair.connect(bob).repay(apeIds.bobTwo, bob.address, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(bob).repay(apeIds.bobTwo, bob.address, 10_000, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.bobTwo)).to.equal(bob.address); // It fails now (because the nonce is no longer a match): @@ -1892,7 +1947,7 @@ describe("NFT Pair", async () => { // This sets both slots of `result`, to [, ]. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); // 2. Sell the NFT, by skimming. // Our mock "market" happens to require a hardcoded amount, because @@ -1965,7 +2020,7 @@ describe("NFT Pair", async () => { // and just send "definitely enough to cover" if we want it to succeed. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); const salePrice1 = getBigNumber(11); // enough to cover the loan actions.push(ACTION_CALL); @@ -1985,7 +2040,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceTwo, apesMarket.address, true])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceTwo, apesMarket.address, 10_000, true])); const salePrice2 = getBigNumber(26); // enough to cover the loan actions.push(ACTION_CALL); @@ -2037,7 +2092,7 @@ describe("NFT Pair", async () => { // This sets both slots of `result`, to [, ]. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "bool"], [apeIds.aliceOne, apesMarket.address, true])); + datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); // 2. Sell the NFT, by skimming. // Our mock "market" happens to require a hardcoded amount, because From 8e418764ac86445e380ccde8163756de0d7a8919 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 7 Jun 2022 04:13:11 +0200 Subject: [PATCH 098/107] (Cache some reads) --- contracts/NFTPair.sol | 7 ++++--- contracts/NFTPairWithOracle.sol | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 6acb427e..74250ac9 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -666,15 +666,16 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // fits in 128 as it represents a BentoBox balance. // Skimming is safe: the amount gets transferred to the lender later, // and therefore cannot be skimmed twice. + IERC20 asset_ = asset; if (skim) { - require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + require(bentoBox.balanceOf(asset_, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); } else { - bentoBox.transfer(asset, msg.sender, address(this), totalShare); + bentoBox.transfer(asset_, msg.sender, address(this), totalShare); } // No overflow: result fits in BentoBox feesEarnedShare += feeShare; // No underflow: `feeShare` is 10% of part of `totalShare` - bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); + bentoBox.transfer(asset_, address(this), lender, totalShare - feeShare); } /// @notice Repay a loan in part or in full diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 3c463d00..8a04c430 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -695,15 +695,16 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // fits in 128 as it represents a BentoBox balance. // Skimming is safe: the amount gets transferred to the lender later, // and therefore cannot be skimmed twice. + IERC20 asset_ = asset; if (skim) { - require(bentoBox.balanceOf(asset, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); + require(bentoBox.balanceOf(asset_, address(this)) >= (totalShare + feesEarnedShare), "NFTPair: skim too much"); } else { - bentoBox.transfer(asset, msg.sender, address(this), totalShare); + bentoBox.transfer(asset_, msg.sender, address(this), totalShare); } // No overflow: result fits in BentoBox feesEarnedShare += feeShare; // No underflow: `feeShare` is 10% of part of `totalShare` - bentoBox.transfer(asset, address(this), lender, totalShare - feeShare); + bentoBox.transfer(asset_, address(this), lender, totalShare - feeShare); } /// @notice Repay a loan in part or in full From 05cd5283acf18e3d9a08e527ff003f7773a45990 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 7 Jun 2022 04:15:17 +0200 Subject: [PATCH 099/107] Use principal amount for partial repayments instead --- contracts/NFTPair.sol | 32 ++++++++++----------- contracts/NFTPairWithOracle.sol | 32 ++++++++++----------- test/NFTPair.test.ts | 50 +++++++++++++++++---------------- 3 files changed, 56 insertions(+), 58 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 74250ac9..6ce2ce33 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -599,8 +599,8 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { function _repayBefore( uint256 tokenId, + uint256 principal, address to, - uint256 partBPS, bool skim ) private @@ -622,20 +622,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { "NFTPair: loan expired" ); - uint256 principal = loanParams.valuation; - if (partBPS < BPS) { - // Math is safe: principal fits in 128 bits - principal = (principal * partBPS) / BPS; - // Math and cast are safe: principal is less than valuation - loanParams.valuation = uint128(loanParams.valuation - principal); - tokenLoanParams[tokenId] = loanParams; - emit LogUpdateLoanParams(tokenId, loanParams); - } else { + if (principal == 0 || principal >= loanParams.valuation) { + principal = loanParams.valuation; // Not following checks-effects-interaction: we are already not // doing that by splitting `repay()` like this; we'll have to trust // the collateral contract if we are to support flash repayments. _finalizeLoan(tokenId, to); emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } else { + // Math and cast are safe: 0 < principal < loanParams.valuation + loanParams.valuation = uint128(loanParams.valuation - principal); + tokenLoanParams[tokenId] = loanParams; + emit LogUpdateLoanParams(tokenId, loanParams); } // No underflow: loan.startTime is only ever set to a block timestamp @@ -680,16 +678,16 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { /// @notice Repay a loan in part or in full /// @param tokenId Token ID of the loan in question. + /// @param principal How much of the principal to repay. Saturates at the full loan value. Zero also taken to mean 100%. /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. - /// @param partBPS Part of the loan to repay, in BPS. (Saturates at 10k). /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function repay( uint256 tokenId, + uint256 principal, address to, - uint16 partBPS, bool skim ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to, partBPS, skim); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, principal, to, skim); _repayAfter(lender, totalShare, feeShare, skim); } @@ -706,7 +704,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { address excessRecipient, bool skimShortage ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller), BPS, false); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, 0, address(seller), false); // External call is safe: At this point the loan is already gone, the // seller has the token, and an amount must be paid via skimming or the @@ -875,14 +873,14 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { uint256 tokenId; uint256 totalShare; uint256 feeShare; + uint256 principal; address lender; - uint16 partBPS; bool skim; { address to; // No skimming, but it can sill be done - (tokenId, to, partBPS, skim) = abi.decode(datas[i], (uint256, address, uint16, bool)); - (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to, partBPS, skim); + (tokenId, principal, to, skim) = abi.decode(datas[i], (uint256, uint256, address, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, principal, to, skim); // Delaying asset collection until after the rest of the // cook is safe: after checking.. - `feesEarnedShare` is // updated after the check - The rest (`totalShare - diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 8a04c430..ad7cc70c 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -628,8 +628,8 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { function _repayBefore( uint256 tokenId, + uint256 principal, address to, - uint256 partBPS, bool skim ) private @@ -651,20 +651,18 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { "NFTPair: loan expired" ); - uint256 principal = loanParams.valuation; - if (partBPS < BPS) { - // Math is safe: principal fits in 128 bits - principal = (principal * partBPS) / BPS; - // Math and cast are safe: principal is less than valuation - loanParams.valuation = uint128(loanParams.valuation - principal); - tokenLoanParams[tokenId] = loanParams; - emit LogUpdateLoanParams(tokenId, loanParams); - } else { + if (principal == 0 || principal >= loanParams.valuation) { + principal = loanParams.valuation; // Not following checks-effects-interaction: we are already not // doing that by splitting `repay()` like this; we'll have to trust // the collateral contract if we are to support flash repayments. _finalizeLoan(tokenId, to); emit LogRepay(skim ? address(this) : msg.sender, tokenId); + } else { + // Math and cast are safe: 0 < principal < loanParams.valuation + loanParams.valuation = uint128(loanParams.valuation - principal); + tokenLoanParams[tokenId] = loanParams; + emit LogUpdateLoanParams(tokenId, loanParams); } // No underflow: loan.startTime is only ever set to a block timestamp @@ -709,16 +707,16 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { /// @notice Repay a loan in part or in full /// @param tokenId Token ID of the loan in question. + /// @param principal How much of the principal to repay. Saturates at the full loan value. Zero also taken to mean 100%. /// @param to Recipient of the returned collateral. Can be anyone if msg.sender is the borrower, otherwise the borrower. - /// @param partBPS Part of the loan to repay, in BPS. (Saturates at 10k). /// @param skim True if the funds have already been Bento-transfered to the contract. Take care to send enough; interest accumulates by the second. function repay( uint256 tokenId, + uint256 principal, address to, - uint16 partBPS, bool skim ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, to, partBPS, skim); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, principal, to, skim); _repayAfter(lender, totalShare, feeShare, skim); } @@ -735,7 +733,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { address excessRecipient, bool skimShortage ) external { - (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, address(seller), BPS, false); + (uint256 totalShare, , uint256 feeShare, address lender) = _repayBefore(tokenId, 0, address(seller), false); // External call is safe: At this point the loan is already gone, the // seller has the token, and an amount must be paid via skimming or the @@ -904,14 +902,14 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { uint256 tokenId; uint256 totalShare; uint256 feeShare; + uint256 principal; address lender; - uint16 partBPS; bool skim; { address to; // No skimming, but it can sill be done - (tokenId, to, partBPS, skim) = abi.decode(datas[i], (uint256, address, uint16, bool)); - (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, to, partBPS, skim); + (tokenId, principal, to, skim) = abi.decode(datas[i], (uint256, uint256, address, bool)); + (totalShare, result[1], feeShare, lender) = _repayBefore(tokenId, principal, to, skim); // Delaying asset collection until after the rest of the // cook is safe: after checking.. - `feesEarnedShare` is // updated after the check - The rest (`totalShare - diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index f5cfcd73..c1937de4 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -708,13 +708,18 @@ describe("NFT Pair", async () => { // happens: const YEAR_BPS = YEAR * 10_000; const COMPOUND_TERMS = 6; - const getMaxRepayShare = (time, params_, part = 10_000) => { + const getMaxRepayShare = (time, params_, overrideValuation: BigNumberish | null = null) => { // We mimic what the contract does, but without rounding errors in the // approximation, so the upper bound should be strict. // 1. Calculate exact amount owed; round it down, like the contract does. // 2. Convert that to Bento shares (still hardcoded at 9/20); rounding up const x = BigRational.from(time * params_.annualInterestBPS).div(YEAR_BPS); - return expApprox(x, COMPOUND_TERMS).mul(params_.valuation).mul(part).div(10_000).floor().mul(9).add(19).div(20); + return expApprox(x, COMPOUND_TERMS) + .mul(overrideValuation ?? params_.valuation) + .floor() + .mul(9) + .add(19) + .div(20); }; before(async () => { @@ -742,7 +747,7 @@ describe("NFT Pair", async () => { // Two Bento transfers: payment to the lender, fee to the contract await advanceNextTime(DAY); - await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, 10_000, false)) + await expect(pair.connect(alice).repay(apeIds.aliceOne, 0, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -784,7 +789,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await advanceNextTime(DAY); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, false)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, 0, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(carol.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -835,7 +840,7 @@ describe("NFT Pair", async () => { const t0 = await getBalances(); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, true)) + await expect(pair.connect(carol).repay(apeIds.aliceOne, 0, alice.address, true)) .to.emit(pair, "LogRepay") .withArgs(pair.address, apeIds.aliceOne) .to.emit(apes, "Transfer") @@ -891,7 +896,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, alice.address, 10_000, false])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceOne, 0, alice.address, false])); await expect(pair.connect(carol).cook(actions, values, datas)) .to.emit(pair, "LogRepay") @@ -956,7 +961,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, alice.address, 10_000, true])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceOne, 0, alice.address, true])); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); await expect(pair.connect(carol).cook(actions, values, datas)) @@ -1021,7 +1026,7 @@ describe("NFT Pair", async () => { const inFive = await advanceNextTime(fiveYears); - await expect(pair.connect(alice).repay(apeIds.aliceTwo, alice.address, 10_000, false)) + await expect(pair.connect(alice).repay(apeIds.aliceTwo, 0, alice.address, false)) .to.emit(pair, "LogRepay") .withArgs(alice.address, apeIds.aliceTwo) .to.emit(apes, "Transfer") @@ -1059,12 +1064,12 @@ describe("NFT Pair", async () => { it("Should refuse repayments on expired loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); - await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, 10_000, false)).to.be.revertedWith("NFTPair: loan expired"); + await expect(pair.connect(alice).repay(apeIds.aliceOne, 0, alice.address, false)).to.be.revertedWith("NFTPair: loan expired"); }); it("Should refuse repayments on nonexistent loans", async () => { await ethers.provider.send("evm_setNextBlockTimestamp", [startTime + params.duration + 1]); - await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, 10_000, false)).to.be.revertedWith("NFTPair: no loan"); + await expect(pair.connect(carol).repay(apeIds.carolOne, 0, carol.address, false)).to.be.revertedWith("NFTPair: no loan"); }); it("Should refuse to skim too much", async () => { @@ -1079,7 +1084,7 @@ describe("NFT Pair", async () => { await bentoBox.connect(bob).transfer(guineas.address, bob.address, pair.address, notEnoughShare); await ethers.provider.send("evm_setNextBlockTimestamp", [(await pair.tokenLoan(apeIds.aliceOne)).startTime.toNumber() + interval]); - await expect(pair.connect(carol).repay(apeIds.aliceOne, alice.address, 10_000, true)).to.be.revertedWith("NFTPair: skim too much"); + await expect(pair.connect(carol).repay(apeIds.aliceOne, 0, alice.address, true)).to.be.revertedWith("NFTPair: skim too much"); }); it("Should allow partial repayments", async () => { @@ -1091,20 +1096,17 @@ describe("NFT Pair", async () => { }); const t0 = await getBalances(); - const part = 1337; // Parts out of 10k that get paid off + const part = params.valuation.mul(1337).div(10_000); const afterParams = { ...params, // We round down when calculating how much to repay, so round up here: - valuation: params.valuation - .mul(10_000 - part) - .add(9999) - .div(10_000), + valuation: params.valuation.sub(part), }; // Two Bento transfers: payment to the lender, fee to the contract await advanceNextTime(DAY); - await expect(pair.connect(alice).repay(apeIds.aliceOne, alice.address, part, false)) + await expect(pair.connect(alice).repay(apeIds.aliceOne, part, alice.address, false)) .to.emit(pair, "LogUpdateLoanParams") .withArgs(apeIds.aliceOne, paramsArray(afterParams)) .to.emit(bentoBox, "LogTransfer") @@ -1116,7 +1118,7 @@ describe("NFT Pair", async () => { const t1 = await getBalances(); const maxRepayShare = getMaxRepayShare(DAY, params, part); - const partValuationShare = valuationShare.mul(part).div(10_000); + const partValuationShare = valuationShare.mul(part).div(params.valuation); const linearInterest = partValuationShare.mul(params.annualInterestBPS).mul(DAY).div(YEAR_BPS); const paid = t0.alice.sub(t1.alice); @@ -1320,7 +1322,7 @@ describe("NFT Pair", async () => { await expect(pair.connect(carol).requestAndBorrow(...successParams)).to.emit(pair, "LogLend"); // Carol repays the loan to get the token back: - await expect(pair.connect(carol).repay(apeIds.carolOne, carol.address, 10_000, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(carol).repay(apeIds.carolOne, 0, carol.address, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.carolOne)).to.equal(carol.address); // It fails now (because the nonce is no longer a match): @@ -1452,7 +1454,7 @@ describe("NFT Pair", async () => { ); // Bob repays the loan to get the token back: - await expect(pair.connect(bob).repay(apeIds.bobTwo, bob.address, 10_000, false)).to.emit(pair, "LogRepay"); + await expect(pair.connect(bob).repay(apeIds.bobTwo, 0, bob.address, false)).to.emit(pair, "LogRepay"); expect(await apes.ownerOf(apeIds.bobTwo)).to.equal(bob.address); // It fails now (because the nonce is no longer a match): @@ -1947,7 +1949,7 @@ describe("NFT Pair", async () => { // This sets both slots of `result`, to [, ]. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceOne, 0, apesMarket.address, true])); // 2. Sell the NFT, by skimming. // Our mock "market" happens to require a hardcoded amount, because @@ -2020,7 +2022,7 @@ describe("NFT Pair", async () => { // and just send "definitely enough to cover" if we want it to succeed. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceOne, 0, apesMarket.address, true])); const salePrice1 = getBigNumber(11); // enough to cover the loan actions.push(ACTION_CALL); @@ -2040,7 +2042,7 @@ describe("NFT Pair", async () => { actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceTwo, apesMarket.address, 10_000, true])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceTwo, 0, apesMarket.address, true])); const salePrice2 = getBigNumber(26); // enough to cover the loan actions.push(ACTION_CALL); @@ -2092,7 +2094,7 @@ describe("NFT Pair", async () => { // This sets both slots of `result`, to [, ]. actions.push(ACTION_REPAY); values.push(0); - datas.push(encodeParameters(["uint256", "address", "uint16", "bool"], [apeIds.aliceOne, apesMarket.address, 10_000, true])); + datas.push(encodeParameters(["uint256", "uint256", "address", "bool"], [apeIds.aliceOne, 0, apesMarket.address, true])); // 2. Sell the NFT, by skimming. // Our mock "market" happens to require a hardcoded amount, because From f51b09152baffdea4eea0876307713f78a5f53ed Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 9 Jun 2022 05:58:50 +0200 Subject: [PATCH 100/107] (Cache another read of asset) --- contracts/NFTPair.sol | 7 +- contracts/NFTPairWithOracle.sol | 7 +- test/PrivatePool.forking.test.ts | 34 ++- test/PrivatePool.test.ts | 411 ++++++++++++++++++++++++------- 4 files changed, 355 insertions(+), 104 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 6ce2ce33..75951f9d 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -716,17 +716,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { // then either `_repayAfter()` will fail, or the funds were sitting in // the contract's BentoBox balance unaccounted for, and could be freely // skimmed for another purpose anyway. - uint256 priceShare = seller.sell(collateral, tokenId, asset, price, address(this)); + IERC20 asset_ = asset; + uint256 priceShare = seller.sell(collateral, tokenId, asset_, price, address(this)); if (priceShare < totalShare) { // No overflow: `totalShare` fits or `_repayAfter()` reverts. See // comments there for proof. // If we are skimming, then we defer the check to `_repayAfter()`, // which checks that the full amount (`totalShare`) has been sent. if (!skimShortage) { - bentoBox.transfer(asset, msg.sender, address(this), totalShare - priceShare); + bentoBox.transfer(asset_, msg.sender, address(this), totalShare - priceShare); } } else if (priceShare > totalShare) { - bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); + bentoBox.transfer(asset_, address(this), excessRecipient, priceShare - totalShare); } _repayAfter(lender, totalShare, feeShare, true); } diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index ad7cc70c..ea442db6 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -745,17 +745,18 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { // then either `_repayAfter()` will fail, or the funds were sitting in // the contract's BentoBox balance unaccounted for, and could be freely // skimmed for another purpose anyway. - uint256 priceShare = seller.sell(collateral, tokenId, asset, price, address(this)); + IERC20 asset_ = asset; + uint256 priceShare = seller.sell(collateral, tokenId, asset_, price, address(this)); if (priceShare < totalShare) { // No overflow: `totalShare` fits or `_repayAfter()` reverts. See // comments there for proof. // If we are skimming, then we defer the check to `_repayAfter()`, // which checks that the full amount (`totalShare`) has been sent. if (!skimShortage) { - bentoBox.transfer(asset, msg.sender, address(this), totalShare - priceShare); + bentoBox.transfer(asset_, msg.sender, address(this), totalShare - priceShare); } } else if (priceShare > totalShare) { - bentoBox.transfer(asset, address(this), excessRecipient, priceShare - totalShare); + bentoBox.transfer(asset_, address(this), excessRecipient, priceShare - totalShare); } _repayAfter(lender, totalShare, feeShare, true); } diff --git a/test/PrivatePool.forking.test.ts b/test/PrivatePool.forking.test.ts index a56d02c6..33d6962b 100644 --- a/test/PrivatePool.forking.test.ts +++ b/test/PrivatePool.forking.test.ts @@ -1,4 +1,10 @@ -import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; +import { + ethers, + network, + deployments, + getNamedAccounts, + artifacts, +} from "hardhat"; import { expect } from "chai"; import { BigNumberish, Signer } from "ethers"; import _ from "lodash"; @@ -36,7 +42,10 @@ const typeDefaults = { // These rely on JS/TS iterating over the keys in the order they were defined: const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); const encodeInitData = (kvs) => - ethers.utils.defaultAbiCoder.encode([`tuple(${initTypeString})`], [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)]); + ethers.utils.defaultAbiCoder.encode( + [`tuple(${initTypeString})`], + [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] + ); const getSignerFor = async (addr) => { await impersonate(addr); @@ -53,9 +62,13 @@ describe("Private Lending Pool - Forked Mainnet", async () => { let generalWhale: Signer; const deployPair = async (initSettings) => { - const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); + const deployTx = await bentoBox + .deploy(masterContract.address, encodeInitData(initSettings), false) + .then((tx) => tx.wait()); const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal("LogDeploy(address,bytes,address)"); + expect(deployEvent.eventSignature).to.equal( + "LogDeploy(address,bytes,address)" + ); const { cloneAddress } = deployEvent.args; return ethers.getContractAt("PrivatePool", cloneAddress); }; @@ -67,7 +80,9 @@ describe("Private Lending Pool - Forked Mainnet", async () => { params: [ { forking: { - jsonRpcUrl: process.env.ETHEREUM_RPC_URL || `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, + jsonRpcUrl: + process.env.ETHEREUM_RPC_URL || + `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, blockNumber: 13715035, }, }, @@ -76,7 +91,10 @@ describe("Private Lending Pool - Forked Mainnet", async () => { await deployments.fixture(["PrivatePool"]); masterContract = await ethers.getContract("PrivatePool"); - bentoBox = await ethers.getContractAt("BentoBoxV1", await masterContract.bentoBox()); + bentoBox = await ethers.getContractAt( + "BentoBoxV1", + await masterContract.bentoBox() + ); const sevenPercentAnnually = getBigNumber(7).div(100 * 3600 * 24 * 365); pairContract = await deployPair({ @@ -135,7 +153,9 @@ describe("Private Lending Pool - Forked Mainnet", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); + await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( + "PrivatePool: already initialized" + ); }); }); }); diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index 0d76a7e1..7591f8d5 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -1,9 +1,27 @@ -import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; +import { + ethers, + network, + deployments, + getNamedAccounts, + artifacts, +} from "hardhat"; import { expect } from "chai"; import { BigNumberish, Signer } from "ethers"; -import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; -import { BentoBoxMock, ERC20Mock, OracleMock, WETH9Mock, PrivatePool } from "../typechain"; +import { + advanceNextTime, + duration, + encodeParameters, + getBigNumber, + impersonate, +} from "../utilities"; +import { + BentoBoxMock, + ERC20Mock, + OracleMock, + WETH9Mock, + PrivatePool, +} from "../typechain"; import { encodeInitData } from "./PrivatePool"; const { formatUnits } = ethers.utils; @@ -84,9 +102,13 @@ describe("Private Lending Pool", async () => { }); const deployPair = async (initSettings) => { - const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); + const deployTx = await bentoBox + .deploy(masterContract.address, encodeInitData(initSettings), false) + .then((tx) => tx.wait()); const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal("LogDeploy(address,bytes,address)"); + expect(deployEvent.eventSignature).to.equal( + "LogDeploy(address,bytes,address)" + ); const { cloneAddress } = deployEvent.args; return ethers.getContractAt("PrivatePool", cloneAddress); }; @@ -101,10 +123,20 @@ describe("Private Lending Pool", async () => { ["Guineas", guineas], ]) { const balance = await contract.balanceOf(acc.address); - const bentoShares = await bentoBox.balanceOf(contract.address, acc.address); - const bentoBalance = await bentoBox.toAmount(contract.address, bentoShares, false); + const bentoShares = await bentoBox.balanceOf( + contract.address, + acc.address + ); + const bentoBalance = await bentoBox.toAmount( + contract.address, + bentoShares, + false + ); console.log(`${token}: ${formatUnits(balance)}`); - console.log(`${token} (BentoBox):`, `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n`); + console.log( + `${token} (BentoBox):`, + `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n` + ); } } }; @@ -231,12 +263,16 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); + await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith( + "PrivatePool: already initialized" + ); }); it("Should not exceed the EIP-170 size limit", async () => { // ..or just rely on Hardhat? - const code = await ethers.provider.send("eth_getCode", [masterContract.address]); + const code = await ethers.provider.send("eth_getCode", [ + masterContract.address, + ]); // Hex string, starting with "0x": const byteCount = (code.length - 2) / 2; expect(byteCount).to.be.lte(0x6000); @@ -263,7 +299,9 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(mainPair.connect(alice).addAsset(true, share)).to.emit(mainPair, "LogAddAsset").withArgs(bentoBox.address, share); + await expect(mainPair.connect(alice).addAsset(true, share)) + .to.emit(mainPair, "LogAddAsset") + .withArgs(bentoBox.address, share); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); @@ -281,7 +319,10 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; const datas = [ - encodeParameters(["address", "address", "uint256", "uint256"], [g, a, amount, 0]), + encodeParameters( + ["address", "address", "uint256", "uint256"], + [g, a, amount, 0] + ), encodeParameters(["int256", "bool"], [share, false]), ]; const values = [0, 0]; @@ -309,7 +350,9 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(mainPair.connect(alice).addAsset(true, share.add(1))).to.be.revertedWith("PrivatePool: skim too much"); + await expect( + mainPair.connect(alice).addAsset(true, share.add(1)) + ).to.be.revertedWith("PrivatePool: skim too much"); }); it("Should let anyone add assets", async () => { @@ -377,7 +420,9 @@ describe("Private Lending Pool", async () => { it("Should refuse collateral for unapproved borrowers", async () => { const share = getBigNumber(55); const to = alice.address; - await expect(mainPair.connect(bob).addCollateral(to, false, share)).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect( + mainPair.connect(bob).addCollateral(to, false, share) + ).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should let approved borrowers add collateral (skim)", async () => { @@ -438,7 +483,9 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to lend to unapproved borrowers", async () => { - await expect(mainPair.connect(alice).borrow(alice.address, 1)).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect( + mainPair.connect(alice).borrow(alice.address, 1) + ).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should enforce LTV requirements when borrowing", async () => { @@ -450,14 +497,21 @@ describe("Private Lending Pool", async () => { const collatAmount = collatShare1.mul(531).div(700); // WETH ratio const borrowAmount = collatAmount.mul(9); // 75% of 12 - await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect( + mainPair.connect(bob).borrow(bob.address, borrowAmount) + ).to.be.revertedWith("PrivatePool: borrower insolvent"); // Accounting for the 0.1% open fee is enough to make it succeed: const withFee = borrowAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit(mainPair, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit( + mainPair, + "LogBorrow" + ); // Borrowing even one more wei is enough to make it fail again: - await expect(mainPair.connect(bob).borrow(bob.address, withFee.add(1))).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect( + mainPair.connect(bob).borrow(bob.address, withFee.add(1)) + ).to.be.revertedWith("PrivatePool: borrower insolvent"); }); it("Should collect the protocol fee immediately", async () => { @@ -477,7 +531,9 @@ describe("Private Lending Pool", async () => { const protocolFeeShare = protocolFee.mul(9).div(20); const takenShare = borrowShare.add(protocolFeeShare); - await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.emit(mainPair, "LogBorrow"); + await expect( + mainPair.connect(bob).borrow(bob.address, borrowAmount) + ).to.emit(mainPair, "LogBorrow"); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(assetShare.sub(takenShare)); @@ -489,24 +545,35 @@ describe("Private Lending Pool", async () => { it("Should not lend out more than there is", async () => { // There are 1000 guineas of assets; 150 WETH allows for borrowing // 1350 and is therefore enough: - await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); + await mainPair + .connect(bob) + .addCollateral(bob.address, false, getBigNumber(150)); // More than reserves - await expect(mainPair.connect(bob).borrow(bob.address, getBigNumber(1001))).to.be.revertedWith("BoringMath: Underflow"); + await expect( + mainPair.connect(bob).borrow(bob.address, getBigNumber(1001)) + ).to.be.revertedWith("BoringMath: Underflow"); }); it("Should not defer the protocol fee on new loans", async () => { // This amounts to testing that the amount + protocol fee need to be in // reserve: - await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); + await mainPair + .connect(bob) + .addCollateral(bob.address, false, getBigNumber(150)); // Exactly the reserves (our test amounts divide cleanly), but that is // not enough with the fee: const reservesAmount = getBigNumber(1000); - await expect(mainPair.connect(bob).borrow(bob.address, reservesAmount)).to.be.revertedWith("BoringMath: Underflow"); + await expect( + mainPair.connect(bob).borrow(bob.address, reservesAmount) + ).to.be.revertedWith("BoringMath: Underflow"); const cutoff = reservesAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit(mainPair, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit( + mainPair, + "LogBorrow" + ); }); }); @@ -547,13 +614,20 @@ describe("Private Lending Pool", async () => { await advanceNextTime(YEAR); const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = debtAmount1.mul(MainTestSettings.INTEREST_PER_SECOND).mul(YEAR).div(getBigNumber(1)); + const extraAmount = debtAmount1 + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(YEAR) + .div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()) + .to.emit(mainPair, "LogAccrue") + .withArgs(extraAmount, feeAmount); // Protocol cut of the open fee + fee on interest, both in shares: - expect((await mainPair.assetBalance()).feesEarnedShare).to.equal(openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20))); + expect((await mainPair.assetBalance()).feesEarnedShare).to.equal( + openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20)) + ); expect(await mainPair.feesOwedAmount()).to.equal(0); const totalDebt = await mainPair.totalDebt(); @@ -564,13 +638,17 @@ describe("Private Lending Pool", async () => { // all the rounding, this should be roughly 0.07007 times the amount // taken out. Since the contract rounds down, we expect to be under, so // round up for the test: - expect(extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1)).to.equal(7007); + expect( + extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1) + ).to.equal(7007); }); it("Should not do anything if nothing is borrowed", async () => { await mainPair.accrue(); // No "LogAccrue" event. Cleaner way to do this? - expect(await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }])).to.deep.equal([]); + expect( + await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }]) + ).to.deep.equal([]); }); it("Should defer fees if everything is loaned out", async () => { @@ -580,7 +658,9 @@ describe("Private Lending Pool", async () => { const almostEverything = assetAmount.mul(99).div(100); const openFee = almostEverything.div(1000); const openProtocolFeeShare = openFee.div(10).mul(9).div(20); - const remainingShare = assetShare.sub(almostEverything.mul(9).div(20)).sub(openProtocolFeeShare); + const remainingShare = assetShare + .sub(almostEverything.mul(9).div(20)) + .sub(openProtocolFeeShare); const initialDebt = almostEverything.add(openFee); await mainPair.connect(bob).borrow(bob.address, almostEverything); @@ -590,9 +670,14 @@ describe("Private Lending Pool", async () => { // 7000% interest; the fee is 7% of the initial debt, which is more than // remaining asset reserves. It should still work: const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = initialDebt.mul(MainTestSettings.INTEREST_PER_SECOND).mul(time).div(getBigNumber(1)); + const extraAmount = initialDebt + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(time) + .div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()) + .to.emit(mainPair, "LogAccrue") + .withArgs(extraAmount, feeAmount); // Outstanding debt is recorded normally: const totalDebt = await mainPair.totalDebt(); @@ -603,7 +688,9 @@ describe("Private Lending Pool", async () => { // These already included the protocol fee: const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(0); - expect(assetBalance.feesEarnedShare).to.equal(remainingShare.add(openProtocolFeeShare)); + expect(assetBalance.feesEarnedShare).to.equal( + remainingShare.add(openProtocolFeeShare) + ); // We collected the remaining asset reserves as fee. The rest is owed: const feeShare = feeAmount.mul(9).div(20); @@ -639,12 +726,18 @@ describe("Private Lending Pool", async () => { // with "seize collateral"-type liquidations; then it's the lender. This // may change if we allow modifying the whitelist; then we'll have to // cleanly handle no-longer-whitelisted users. - expect(await mainPair.connect(bob).removeCollateral(bob.address, collatShare1)) + expect( + await mainPair.connect(bob).removeCollateral(bob.address, collatShare1) + ) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(bob.address, bob.address, collatShare1); const remainder = getBigNumber(12); - expect(await mainPair.connect(carol).removeCollateral(carol.address, collatShare2.sub(remainder))) + expect( + await mainPair + .connect(carol) + .removeCollateral(carol.address, collatShare2.sub(remainder)) + ) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(carol.address, carol.address, collatShare2.sub(remainder)); }); @@ -701,7 +794,10 @@ describe("Private Lending Pool", async () => { expect(await mainPair.borrowerDebtPart(bob.address)).to.equal(debtPart); await advanceNextTime(timeStep); - const extraAmount = debtAmount.mul(MainTestSettings.INTEREST_PER_SECOND).mul(timeStep).div(one); + const extraAmount = debtAmount + .mul(MainTestSettings.INTEREST_PER_SECOND) + .mul(timeStep) + .div(one); debtAmount = debtAmount.add(extraAmount); // "parts" are in units of the initial debt. These should cover it: @@ -710,7 +806,10 @@ describe("Private Lending Pool", async () => { // Bob owns all the debt, so this is the conversion. Rounding is up, in // favour of the contract, so that the amount definitely covers the // part intended to be paid back: - const repayAmount = repayPart.mul(debtAmount).add(debtPart.sub(1)).div(debtPart); + const repayAmount = repayPart + .mul(debtAmount) + .add(debtPart.sub(1)) + .div(debtPart); // "Smallest number of shares covering this" -- so rounded up again: // Note that -- as in the UniV2 AMMs, for instance -- this number of @@ -752,11 +851,17 @@ describe("Private Lending Pool", async () => { }; t0.bobDebtPart = t0.bobDebtAmount; - expect(t0.assetBalance.reservesShare).to.equal(assetShare.sub(bobLoanAmount.mul(9).div(20)).sub(bobLoanAmount.div(10_000).mul(9).div(20))); + expect(t0.assetBalance.reservesShare).to.equal( + assetShare + .sub(bobLoanAmount.mul(9).div(20)) + .sub(bobLoanAmount.div(10_000).mul(9).div(20)) + ); expect(t0.totalDebt.elastic).to.equal(t0.bobDebtAmount); const t1 = { - accruedInterest: t0.bobDebtAmount.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), + accruedInterest: t0.bobDebtAmount + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), }; // We find the number of shares Carol should borrow to drain reserves. We // account for the protocol cut of Bob's accrued interest, and Carol's @@ -766,7 +871,11 @@ describe("Private Lending Pool", async () => { // of wiggle room (here and elsewhere); "multiplication and then division // with rounding" is not an invertible operation, and not every target // can be reached no matter how you round. (Try (M * 2) / 1 == 3). - const carolLoanShare = t0.assetBalance.reservesShare.sub(t1.accruedInterest.div(10).mul(9).div(20)).add(1).mul(10_000).div(10_001); + const carolLoanShare = t0.assetBalance.reservesShare + .sub(t1.accruedInterest.div(10).mul(9).div(20)) + .add(1) + .mul(10_000) + .div(10_001); const carolLoanAmount = carolLoanShare.mul(20).div(9).add(2); await advanceNextTime(timeStep); @@ -786,7 +895,9 @@ describe("Private Lending Pool", async () => { await mainPair.accrue(); const t2 = { - accruedInterest: t1.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), + accruedInterest: t1.totalDebt.elastic + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), assetBalance: await mainPair.assetBalance(), totalDebt: await mainPair.totalDebt(), feesOwedAmount: await mainPair.feesOwedAmount(), @@ -797,16 +908,22 @@ describe("Private Lending Pool", async () => { // the error is more than 1 or even `toAmount(1)`: expect(t2.assetBalance.reservesShare).to.equal(0); expect(t2.feesOwedAmount).to.be.gt(0); - expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte(5); + expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte( + 5 + ); // Repaying triggers another accrual, so we (advance a fixed time and) // determine how much will be owed after that: const t3 = { - accruedInterest: t2.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), + accruedInterest: t2.totalDebt.elastic + .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) + .div(one), }; // Intermediate value; we would see it if we did something that accrues // but not deposits any assets: - t3.feesOwedBeforeRepay = t2.feesOwedAmount.add(t3.accruedInterest.div(10)); + t3.feesOwedBeforeRepay = t2.feesOwedAmount.add( + t3.accruedInterest.div(10) + ); // A debt "part" corresponds to 1 token when the first loan is taken out. // When interest accrues this amount grows correspondingly; it never @@ -830,7 +947,11 @@ describe("Private Lending Pool", async () => { // taken out of reserves, they were not "earned" until we made the // repayment. At which point we expect "fees earned" to have increased // by exactly that amount (in shares): - expect(t3.assetBalance.feesEarnedShare).to.equal(t2.assetBalance.feesEarnedShare.add(t3.feesOwedBeforeRepay.mul(9).div(20))); + expect(t3.assetBalance.feesEarnedShare).to.equal( + t2.assetBalance.feesEarnedShare.add( + t3.feesOwedBeforeRepay.mul(9).div(20) + ) + ); expect(t3.feesOwedAmount).to.equal(0); // Debt gets paid off as normal; in particular the fees are not added to @@ -840,11 +961,16 @@ describe("Private Lending Pool", async () => { // The small amount we repaid in excess of the fees owed should have gone // to asset reserves. - const excessRepayAmount = repayPart.mul(t3.totalDebt.elastic).div(t3.totalDebt.base).sub(t3.feesOwedBeforeRepay); + const excessRepayAmount = repayPart + .mul(t3.totalDebt.elastic) + .div(t3.totalDebt.base) + .sub(t3.feesOwedBeforeRepay); const excessRepayShare = excessRepayAmount.mul(9).div(20); // Again, giving it a few wei of leeway due to rounding in _receiveAsset: - expect(t3.assetBalance.reservesShare.sub(excessRepayShare).abs()).to.be.lte(5); + expect( + t3.assetBalance.reservesShare.sub(excessRepayShare).abs() + ).to.be.lte(5); }); }); @@ -869,7 +995,9 @@ describe("Private Lending Pool", async () => { const [a, b, c] = [alice, bob, carol].map((x) => x.address); await mainPair.connect(alice).addAsset(false, assetShare); await mainPair.connect(bob).addCollateral(b, false, bobCollateralShare); - await mainPair.connect(carol).addCollateral(c, false, carolCollateralShare); + await mainPair + .connect(carol) + .addCollateral(c, false, carolCollateralShare); await oracle.set(one.div(10)); // one WETH is 10 guineas await mainPair.updateExchangeRate(); @@ -896,9 +1024,14 @@ describe("Private Lending Pool", async () => { it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect(mainPair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( - "PrivatePool: all are solvent" - ); + await expect( + mainPair.liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.be.revertedWith("PrivatePool: all are solvent"); }); it("Should liquidate insolvent borrowers only", async () => { @@ -913,7 +1046,16 @@ describe("Private Lending Pool", async () => { // Alice has guineas and has approved the contract. That she is also the // lender makes no difference in the execution path taken. - await expect(mainPair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)) + await expect( + mainPair + .connect(alice) + .liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ) .to.emit(mainPair, "LogRemoveCollateral") .to.emit(mainPair, "LogRepay"); @@ -928,7 +1070,10 @@ describe("Private Lending Pool", async () => { assetBalance: await mainPair.assetBalance(), - aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), + aliceBentoGuineas: await bentoBox.balanceOf( + guineas.address, + alice.address + ), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -953,28 +1098,45 @@ describe("Private Lending Pool", async () => { const minRepayShare = bobLiquidatePart.mul(9).div(20); // Not entirely accurate because it gets calculated differently, but // equivalent up to rounding effects: - const protocolFeeShare = minRepayShare.mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS).div(10_000).sub(minRepayShare).div(10); - const aliceMaxBentoGuineas = t0.aliceBentoGuineas.sub(minRepayShare).sub(protocolFeeShare); + const protocolFeeShare = minRepayShare + .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) + .div(10_000) + .sub(minRepayShare) + .div(10); + const aliceMaxBentoGuineas = t0.aliceBentoGuineas + .sub(minRepayShare) + .sub(protocolFeeShare); expect(t1.aliceBentoGuineas).to.be.lte(aliceMaxBentoGuineas); - expect(t1.aliceBentoGuineas).to.be.gte(aliceMaxBentoGuineas.mul(9999).div(10_000)); + expect(t1.aliceBentoGuineas).to.be.gte( + aliceMaxBentoGuineas.mul(9999).div(10_000) + ); - const aliceMinBentoWeth = t0.aliceBentoWeth.add(bobMinCollateralTakenShare); + const aliceMinBentoWeth = t0.aliceBentoWeth.add( + bobMinCollateralTakenShare + ); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); + expect(t1.aliceBentoWeth).to.be.lte( + aliceMinBentoWeth.mul(10_001).div(10_000) + ); // If we want a firm lower bound on asset reserves, we need to account // for interest: the accrue() call right before liquidations charges // interest, and takes the protocol cut of that interest out of reserves. // We divide rounding up: - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( + t1.blockTimestamp - t0.blockTimestamp + ) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = t0.assetBalance.reservesShare.add(minRepayShare).sub(maxInterestFee); - const minFeesEarnedShare = t0.assetBalance.feesEarnedShare.add(protocolFeeShare); + const minAssetReserves = t0.assetBalance.reservesShare + .add(minRepayShare) + .sub(maxInterestFee); + const minFeesEarnedShare = + t0.assetBalance.feesEarnedShare.add(protocolFeeShare); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); expect(t1.assetBalance.feesEarnedShare).to.be.gte(minFeesEarnedShare); @@ -984,13 +1146,21 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); + expect(t1.totalDebt.base).to.equal( + t0.totalDebt.base.sub(bobLiquidatePart) + ); - const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); + const bobMaxCollateralShare = t0.bobCollateralShare.sub( + bobMinCollateralTakenShare + ); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); + expect(t1.bobCollateralShare).to.be.gte( + bobMaxCollateralShare.mul(9999).div(10_000) + ); // Equivalent check.. - expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare)); + expect(t1.collateralBalance.userTotalShare).to.equal( + t1.bobCollateralShare.add(t1.carolCollateralShare) + ); }); }); @@ -1054,9 +1224,14 @@ describe("Private Lending Pool", async () => { it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect(pair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( - "PrivatePool: all are solvent" - ); + await expect( + pair.liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.be.revertedWith("PrivatePool: all are solvent"); }); it("Should liquidate insolvent borrowers only", async () => { @@ -1071,10 +1246,16 @@ describe("Private Lending Pool", async () => { // That Alice she is also the lender makes no difference in the execution // path taken. - await expect(pair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.emit( - pair, - "LogSeizeCollateral" - ); + await expect( + pair + .connect(alice) + .liquidate( + [bob.address, carol.address], + [one, one], + alice.address, + AddressZero + ) + ).to.emit(pair, "LogSeizeCollateral"); const t1 = { totalDebt: await pair.totalDebt(), @@ -1088,7 +1269,10 @@ describe("Private Lending Pool", async () => { assetBalance: await pair.assetBalance(), collateralBalance: await pair.collateralBalance(), - aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), + aliceBentoGuineas: await bentoBox.balanceOf( + guineas.address, + alice.address + ), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -1122,8 +1306,12 @@ describe("Private Lending Pool", async () => { .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) .add(minCollateralLiquidatorShare); - expect(t1.collateralBalance.feesEarnedShare).to.be.gte(minCollateralFeeShare); - expect(t1.collateralBalance.feesEarnedShare).to.be.lte(minCollateralFeeShare.mul(10_001).div(10_000)); + expect(t1.collateralBalance.feesEarnedShare).to.be.gte( + minCollateralFeeShare + ); + expect(t1.collateralBalance.feesEarnedShare).to.be.lte( + minCollateralFeeShare.mul(10_001).div(10_000) + ); // Alice gets the liquidator part of the bonus only, in kind, minus the // protocol fee. The contract gets the protocol fee over the bonus. @@ -1131,22 +1319,31 @@ describe("Private Lending Pool", async () => { expect(t1.aliceBentoGuineas).to.equal(t0.aliceBentoGuineas); // Alice the liquidator: - const aliceMinBentoWeth = t0.aliceBentoWeth.add(minCollateralLiquidatorShare); + const aliceMinBentoWeth = t0.aliceBentoWeth.add( + minCollateralLiquidatorShare + ); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); + expect(t1.aliceBentoWeth).to.be.lte( + aliceMinBentoWeth.mul(10_001).div(10_000) + ); // Alice the lender: expect(t1.aliceCollateralShare).to.be.gte(minCollateralLenderShare); - expect(t1.aliceCollateralShare).to.be.lte(minCollateralLenderShare.mul(10_001).div(10_000)); + expect(t1.aliceCollateralShare).to.be.lte( + minCollateralLenderShare.mul(10_001).div(10_000) + ); // Asset reserves do not really change, except for the interest fee.. - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( + t1.blockTimestamp - t0.blockTimestamp + ) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = t0.assetBalance.reservesShare.sub(maxInterestFee); + const minAssetReserves = + t0.assetBalance.reservesShare.sub(maxInterestFee); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); // Carol was not insolvent, so that liquidation failed: @@ -1155,13 +1352,23 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); + expect(t1.totalDebt.base).to.equal( + t0.totalDebt.base.sub(bobLiquidatePart) + ); - const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); + const bobMaxCollateralShare = t0.bobCollateralShare.sub( + bobMinCollateralTakenShare + ); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); + expect(t1.bobCollateralShare).to.be.gte( + bobMaxCollateralShare.mul(9999).div(10_000) + ); // Given that individual shares are as expected, this tests the total: - expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare).add(t1.aliceCollateralShare)); + expect(t1.collateralBalance.userTotalShare).to.equal( + t1.bobCollateralShare + .add(t1.carolCollateralShare) + .add(t1.aliceCollateralShare) + ); }); }); @@ -1191,12 +1398,24 @@ describe("Private Lending Pool", async () => { await coinPair.updateExchangeRate(); await dollar.connect(alice).approve(bentoBox.address, MaxUint256); - await bentoBox.connect(alice).deposit(dollar.address, alice.address, alice.address, totalSupply.div(10), 0); + await bentoBox + .connect(alice) + .deposit( + dollar.address, + alice.address, + alice.address, + totalSupply.div(10), + 0 + ); await coinPair.connect(alice).addAsset(false, totalSupply.div(10)); await coin.connect(bob).approve(bentoBox.address, MaxUint256); - await bentoBox.connect(bob).deposit(coin.address, bob.address, bob.address, totalSupply, 0); - await coinPair.connect(bob).addCollateral(bob.address, false, totalSupply); + await bentoBox + .connect(bob) + .deposit(coin.address, bob.address, bob.address, totalSupply, 0); + await coinPair + .connect(bob) + .addCollateral(bob.address, false, totalSupply); const assetBalance = await coinPair.assetBalance(); const collateralBalance = await coinPair.collateralBalance(); @@ -1210,15 +1429,25 @@ describe("Private Lending Pool", async () => { expect(bentoCoinTotals.elastic).to.equal(totalSupply); if (shouldPass) { - await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.emit(coinPair, "LogBorrow").to.emit(bentoBox, "LogTransfer"); + await expect(coinPair.connect(bob).borrow(bob.address, 1)) + .to.emit(coinPair, "LogBorrow") + .to.emit(bentoBox, "LogTransfer"); } else { - await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.be.revertedWith("BoringMath: Mul Overflow"); + await expect( + coinPair.connect(bob).borrow(bob.address, 1) + ).to.be.revertedWith("BoringMath: Mul Overflow"); } }; - it("Works with all SPELL as collateral (no strat losses)", makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true)); + it( + "Works with all SPELL as collateral (no strat losses)", + makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true) + ); // This fails with the old Kashi-style `isSolvent`: - it("No longer breaks at 393 billion (18-decimal) tokens", makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true)); + it( + "No longer breaks at 393 billion (18-decimal) tokens", + makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true) + ); }); }); From 6a827262ca938cd72a36423420710cf1be5b0198 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 14 Jun 2022 06:12:20 +0200 Subject: [PATCH 101/107] Round required BentoBox shares up for repayments --- contracts/NFTPair.sol | 4 ++-- contracts/NFTPairWithOracle.sol | 4 ++-- test/NFTPair.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index 75951f9d..c602e964 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -647,7 +647,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { Rebase memory bentoBoxTotals = bentoBox.totals(asset); - totalShare = bentoBoxTotals.toBase(totalAmount, false); + totalShare = bentoBoxTotals.toBase(totalAmount, true); feeShare = bentoBoxTotals.toBase(fee, false); lender = loan.lender; } @@ -869,7 +869,7 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { } else if (action == ACTION_GET_SHARES_DUE) { uint256 tokenId = abi.decode(datas[i], (uint256)); result[1] = _getAmountDue(tokenId); - result[0] = bentoBox.toShare(asset, result[1], false); + result[0] = bentoBox.toShare(asset, result[1], true); } else if (action == ACTION_REPAY) { uint256 tokenId; uint256 totalShare; diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index ea442db6..54b3b7f3 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -676,7 +676,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { Rebase memory bentoBoxTotals = bentoBox.totals(asset); - totalShare = bentoBoxTotals.toBase(totalAmount, false); + totalShare = bentoBoxTotals.toBase(totalAmount, true); feeShare = bentoBoxTotals.toBase(fee, false); lender = loan.lender; } @@ -898,7 +898,7 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { } else if (action == ACTION_GET_SHARES_DUE) { uint256 tokenId = abi.decode(datas[i], (uint256)); result[1] = _getAmountDue(tokenId); - result[0] = bentoBox.toShare(asset, result[1], false); + result[0] = bentoBox.toShare(asset, result[1], true); } else if (action == ACTION_REPAY) { uint256 tokenId; uint256 totalShare; diff --git a/test/NFTPair.test.ts b/test/NFTPair.test.ts index c1937de4..fa825298 100644 --- a/test/NFTPair.test.ts +++ b/test/NFTPair.test.ts @@ -823,7 +823,7 @@ describe("NFT Pair", async () => { // an excess left; skimming is really only suitable for contracts that // can calculate the exact repayment needed: const exactAmount = params.valuation.add(await pair.calculateInterest(params.valuation, interval, params.annualInterestBPS)); - // The contract rounds down; we round up and add a little: + // We round up, like the contract, and add a little for good measure: const closeToShare = exactAmount.mul(9).add(19).div(20); const enoughShare = closeToShare.add(getBigNumber(1337, 8)); From d1bfcb1dd1a26abe2424640b4d7e71d88d67497b Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 22 Jun 2022 04:45:16 +0200 Subject: [PATCH 102/107] (Put mock BentoBox contracts back) --- {archive/contracts => contracts}/BentoBoxFlat.sol | 0 {archive/contracts => contracts}/mocks/BentoBoxMock.sol | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {archive/contracts => contracts}/BentoBoxFlat.sol (100%) rename {archive/contracts => contracts}/mocks/BentoBoxMock.sol (100%) diff --git a/archive/contracts/BentoBoxFlat.sol b/contracts/BentoBoxFlat.sol similarity index 100% rename from archive/contracts/BentoBoxFlat.sol rename to contracts/BentoBoxFlat.sol diff --git a/archive/contracts/mocks/BentoBoxMock.sol b/contracts/mocks/BentoBoxMock.sol similarity index 100% rename from archive/contracts/mocks/BentoBoxMock.sol rename to contracts/mocks/BentoBoxMock.sol From 01609d2b9ba0f407fd118bee93d48605ace34996 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 22 Jun 2022 17:39:42 +0200 Subject: [PATCH 103/107] Fix type annotations and format for PrivatePool tests --- test/PrivatePool.forking.test.ts | 69 ++-- test/PrivatePool.test.ts | 584 ++++++++++--------------------- 2 files changed, 231 insertions(+), 422 deletions(-) diff --git a/test/PrivatePool.forking.test.ts b/test/PrivatePool.forking.test.ts index 33d6962b..00e35490 100644 --- a/test/PrivatePool.forking.test.ts +++ b/test/PrivatePool.forking.test.ts @@ -1,10 +1,4 @@ -import { - ethers, - network, - deployments, - getNamedAccounts, - artifacts, -} from "hardhat"; +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; import { expect } from "chai"; import { BigNumberish, Signer } from "ethers"; import _ from "lodash"; @@ -42,17 +36,28 @@ const typeDefaults = { // These rely on JS/TS iterating over the keys in the order they were defined: const initTypeString = _.map(initTypes, (t, name) => `${t} ${name}`).join(", "); const encodeInitData = (kvs) => - ethers.utils.defaultAbiCoder.encode( - [`tuple(${initTypeString})`], - [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)] - ); + ethers.utils.defaultAbiCoder.encode([`tuple(${initTypeString})`], [_.mapValues(initTypes, (t, k) => kvs[k] || typeDefaults[t] || 0)]); const getSignerFor = async (addr) => { await impersonate(addr); return ethers.getSigner(addr); }; -describe("Private Lending Pool - Forked Mainnet", async () => { +interface IPartialDeployParams { + lender?: string; + borrowers?: string[]; + collateral?: string; + asset?: string; + oracle?: string; + INTEREST_PER_SECOND: BigNumberish; + COLLATERALIZATION_RATE_BPS: number; + LIQUIDATION_MULTIPLIER_BPS: number; + BORROW_OPENING_FEE_BPS: number; +} + +const maybe = process.env.FORKING === "true" && (process.env.ETHEREUM_RPC_URL || process.env.INFURA_API_KEY) ? describe : describe.skip; + +maybe("Private Lending Pool - Forked Mainnet", async () => { let snapshotId; let masterContract: PrivatePool; let bentoBox: BentoBoxV1; @@ -61,16 +66,14 @@ describe("Private Lending Pool - Forked Mainnet", async () => { let usdcWhale: Signer; let generalWhale: Signer; - const deployPair = async (initSettings) => { - const deployTx = await bentoBox - .deploy(masterContract.address, encodeInitData(initSettings), false) - .then((tx) => tx.wait()); - const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal( - "LogDeploy(address,bytes,address)" - ); - const { cloneAddress } = deployEvent.args; - return ethers.getContractAt("PrivatePool", cloneAddress); + const deployPair = async (initSettings: IPartialDeployParams) => { + const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); + for (const e of deployTx.events ?? []) { + if (e.eventSignature == "LogDeploy(address,bytes,address)") { + return ethers.getContractAt("PrivatePool", e.args?.cloneAddress); + } + } + throw new Error("Deploy event not found"); // (For the typechecker..) }; before(async () => { @@ -80,9 +83,7 @@ describe("Private Lending Pool - Forked Mainnet", async () => { params: [ { forking: { - jsonRpcUrl: - process.env.ETHEREUM_RPC_URL || - `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, + jsonRpcUrl: process.env.ETHEREUM_RPC_URL || `https://eth-mainnet.alchemyapi.io/v2/${alchemyKey}`, blockNumber: 13715035, }, }, @@ -91,10 +92,7 @@ describe("Private Lending Pool - Forked Mainnet", async () => { await deployments.fixture(["PrivatePool"]); masterContract = await ethers.getContract("PrivatePool"); - bentoBox = await ethers.getContractAt( - "BentoBoxV1", - await masterContract.bentoBox() - ); + bentoBox = await ethers.getContractAt("BentoBoxV1", await masterContract.bentoBox()); const sevenPercentAnnually = getBigNumber(7).div(100 * 3600 * 24 * 365); pairContract = await deployPair({ @@ -133,6 +131,10 @@ describe("Private Lending Pool - Forked Mainnet", async () => { await expect( deployPair({ collateral: ZERO_ADDR, + LIQUIDATION_MULTIPLIER_BPS: 9_999, + COLLATERALIZATION_RATE_BPS: 10_001, + INTEREST_PER_SECOND: getBigNumber(7).div(100 * 3600 * 24 * 365), + BORROW_OPENING_FEE_BPS: 10, }) ).to.be.revertedWith("PrivatePool: bad pair"); @@ -140,6 +142,9 @@ describe("Private Lending Pool - Forked Mainnet", async () => { deployPair({ collateral: WETH9, LIQUIDATION_MULTIPLIER_BPS: 9_999, + COLLATERALIZATION_RATE_BPS: 10_001, + INTEREST_PER_SECOND: getBigNumber(7).div(100 * 3600 * 24 * 365), + BORROW_OPENING_FEE_BPS: 10, }) ).to.be.revertedWith("PrivatePool: negative liquidation bonus"); @@ -148,14 +153,14 @@ describe("Private Lending Pool - Forked Mainnet", async () => { collateral: WETH9, LIQUIDATION_MULTIPLIER_BPS: 10_000, COLLATERALIZATION_RATE_BPS: 10_001, + INTEREST_PER_SECOND: getBigNumber(7).div(100 * 3600 * 24 * 365), + BORROW_OPENING_FEE_BPS: 10, }) ).to.be.revertedWith("PrivatePool: bad collateralization rate"); }); it("Should refuse to initialize twice", async () => { - await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith( - "PrivatePool: already initialized" - ); + await expect(pairContract.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); }); }); }); diff --git a/test/PrivatePool.test.ts b/test/PrivatePool.test.ts index 7591f8d5..a5d888de 100644 --- a/test/PrivatePool.test.ts +++ b/test/PrivatePool.test.ts @@ -1,27 +1,10 @@ -import { - ethers, - network, - deployments, - getNamedAccounts, - artifacts, -} from "hardhat"; +import { ethers, network, deployments, getNamedAccounts, artifacts } from "hardhat"; import { expect } from "chai"; -import { BigNumberish, Signer } from "ethers"; - -import { - advanceNextTime, - duration, - encodeParameters, - getBigNumber, - impersonate, -} from "../utilities"; -import { - BentoBoxMock, - ERC20Mock, - OracleMock, - WETH9Mock, - PrivatePool, -} from "../typechain"; +import { BigNumber, BigNumberish, Contract } from "ethers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; + +import { advanceNextTime, duration, encodeParameters, getBigNumber, impersonate } from "../utilities"; +import { BentoBoxMock, ERC20Mock, OracleMock, WETH9Mock, PrivatePool } from "../typechain"; import { encodeInitData } from "./PrivatePool"; const { formatUnits } = ethers.utils; @@ -58,6 +41,19 @@ const Cook = { USE_VALUE2: -2, }; +interface IPartialDeployParams { + lender?: string; + borrowers?: string[]; + collateral?: string; + asset?: string; + oracle?: string; + INTEREST_PER_SECOND?: BigNumber; + COLLATERALIZATION_RATE_BPS?: number; + LIQUIDATION_MULTIPLIER_BPS?: number; + BORROW_OPENING_FEE_BPS?: number; + LIQUIDATION_SEIZE_COLLATERAL?: boolean; +} + const MainTestSettings = { // 7% annually -- precision is 18 digits: INTEREST_PER_SECOND: getBigNumber(7).div(100 * 3600 * 24 * 365), @@ -73,9 +69,9 @@ describe("Private Lending Pool", async () => { let oracle: OracleMock; let masterContract: PrivatePool; let mainPair: PrivatePool; - let alice: Signer; - let bob: Signer; - let carol: Signer; + let alice: SignerWithAddress; + let bob: SignerWithAddress; + let carol: SignerWithAddress; // Inner snapshots.The "inner" state is whatever `before` in `proc` sets up. // Reverts to the "outer" state after. @@ -101,16 +97,20 @@ describe("Private Lending Pool", async () => { }); }); - const deployPair = async (initSettings) => { - const deployTx = await bentoBox - .deploy(masterContract.address, encodeInitData(initSettings), false) - .then((tx) => tx.wait()); - const [deployEvent] = deployTx.events; - expect(deployEvent.eventSignature).to.equal( - "LogDeploy(address,bytes,address)" - ); - const { cloneAddress } = deployEvent.args; - return ethers.getContractAt("PrivatePool", cloneAddress); + const deployContract = async (name, ...args) => { + const contract = await ethers.getContractFactory(name).then((f) => f.deploy(...args)); + // Simpler way to "cast"? The above works as the result if we igore types.. + return ethers.getContractAt(name, contract.address); + }; + + const deployPair = async (initSettings: IPartialDeployParams) => { + const deployTx = await bentoBox.deploy(masterContract.address, encodeInitData(initSettings), false).then((tx) => tx.wait()); + for (const e of deployTx.events ?? []) { + if (e.eventSignature == "LogDeploy(address,bytes,address)") { + return ethers.getContractAt("PrivatePool", e.args?.cloneAddress); + } + } + throw new Error("Deploy event not found"); // (For the typechecker..) }; const showBalances = async (accs = { alice, bob, carol }) => { @@ -118,45 +118,31 @@ describe("Private Lending Pool", async () => { console.log(`\n---- ${name}:`); const ethBalance = await ethers.provider.getBalance(acc.address); console.log(`Ethereum: ${formatUnits(ethBalance)}\n`); - for (const [token, contract] of [ - ["WETH", weth], - ["Guineas", guineas], - ]) { + async function show(token: string, contract: WETH9Mock | ERC20Mock) { const balance = await contract.balanceOf(acc.address); - const bentoShares = await bentoBox.balanceOf( - contract.address, - acc.address - ); - const bentoBalance = await bentoBox.toAmount( - contract.address, - bentoShares, - false - ); + const bentoShares = await bentoBox.balanceOf(contract.address, acc.address); + const bentoBalance = await bentoBox.toAmount(contract.address, bentoShares, false); console.log(`${token}: ${formatUnits(balance)}`); - console.log( - `${token} (BentoBox):`, - `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n` - ); + console.log(`${token} (BentoBox):`, `${formatUnits(bentoBalance)} (${formatUnits(bentoShares)} shares)\n`); } + await show("WETH", weth); + await show("Guineas", guineas); + // for (const [token, contract] of [ + // ["WETH", weth], + // ["Guineas", guineas], + // ]) { + // } } }; before(async () => { - const WETH9Mock = await ethers.getContractFactory("WETH9Mock"); - weth = await WETH9Mock.deploy(); - - const BentoBoxMock = await ethers.getContractFactory("BentoBoxMock"); - bentoBox = await BentoBoxMock.deploy(weth.address); + weth = await deployContract("WETH9Mock"); + bentoBox = await deployContract("BentoBoxMock", weth.address); - const PrivatePool = await ethers.getContractFactory("PrivatePool"); - masterContract = await PrivatePool.deploy(bentoBox.address); + masterContract = await deployContract("PrivatePool", bentoBox.address); await bentoBox.whitelistMasterContract(masterContract.address, true); - - const ERC20Mock = await ethers.getContractFactory("ERC20Mock"); - guineas = await ERC20Mock.deploy(getBigNumber(1_000_000)); - - const OracleMock = await ethers.getContractFactory("OracleMock"); - oracle = await OracleMock.deploy(); + guineas = await deployContract("ERC20Mock", getBigNumber(1_000_000)); + oracle = await deployContract("OracleMock"); const addresses = await getNamedAccounts(); alice = await ethers.getSigner(addresses.alice); @@ -263,16 +249,12 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to initialize twice", async () => { - await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith( - "PrivatePool: already initialized" - ); + await expect(mainPair.init(encodeInitData({}))).to.be.revertedWith("PrivatePool: already initialized"); }); it("Should not exceed the EIP-170 size limit", async () => { // ..or just rely on Hardhat? - const code = await ethers.provider.send("eth_getCode", [ - masterContract.address, - ]); + const code = await ethers.provider.send("eth_getCode", [masterContract.address]); // Hex string, starting with "0x": const byteCount = (code.length - 2) / 2; expect(byteCount).to.be.lte(0x6000); @@ -299,9 +281,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect(mainPair.connect(alice).addAsset(true, share)) - .to.emit(mainPair, "LogAddAsset") - .withArgs(bentoBox.address, share); + await expect(mainPair.connect(alice).addAsset(true, share)).to.emit(mainPair, "LogAddAsset").withArgs(bentoBox.address, share); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(getBigNumber(450)); @@ -319,10 +299,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); const actions = [Cook.ACTION_BENTO_DEPOSIT, Cook.ACTION_ADD_ASSET]; const datas = [ - encodeParameters( - ["address", "address", "uint256", "uint256"], - [g, a, amount, 0] - ), + encodeParameters(["address", "address", "uint256", "uint256"], [g, a, amount, 0]), encodeParameters(["int256", "bool"], [share, false]), ]; const values = [0, 0]; @@ -350,9 +327,7 @@ describe("Private Lending Pool", async () => { const [g, a, p] = [guineas, alice, mainPair].map((x) => x.address); await bentoBox.connect(alice).transfer(g, a, p, share); - await expect( - mainPair.connect(alice).addAsset(true, share.add(1)) - ).to.be.revertedWith("PrivatePool: skim too much"); + await expect(mainPair.connect(alice).addAsset(true, share.add(1))).to.be.revertedWith("PrivatePool: skim too much"); }); it("Should let anyone add assets", async () => { @@ -420,9 +395,7 @@ describe("Private Lending Pool", async () => { it("Should refuse collateral for unapproved borrowers", async () => { const share = getBigNumber(55); const to = alice.address; - await expect( - mainPair.connect(bob).addCollateral(to, false, share) - ).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect(mainPair.connect(bob).addCollateral(to, false, share)).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should let approved borrowers add collateral (skim)", async () => { @@ -483,9 +456,7 @@ describe("Private Lending Pool", async () => { }); it("Should refuse to lend to unapproved borrowers", async () => { - await expect( - mainPair.connect(alice).borrow(alice.address, 1) - ).to.be.revertedWith("PrivatePool: unapproved borrower"); + await expect(mainPair.connect(alice).borrow(alice.address, 1)).to.be.revertedWith("PrivatePool: unapproved borrower"); }); it("Should enforce LTV requirements when borrowing", async () => { @@ -497,21 +468,14 @@ describe("Private Lending Pool", async () => { const collatAmount = collatShare1.mul(531).div(700); // WETH ratio const borrowAmount = collatAmount.mul(9); // 75% of 12 - await expect( - mainPair.connect(bob).borrow(bob.address, borrowAmount) - ).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.be.revertedWith("PrivatePool: borrower insolvent"); // Accounting for the 0.1% open fee is enough to make it succeed: const withFee = borrowAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit( - mainPair, - "LogBorrow" - ); + await expect(mainPair.connect(bob).borrow(bob.address, withFee)).to.emit(mainPair, "LogBorrow"); // Borrowing even one more wei is enough to make it fail again: - await expect( - mainPair.connect(bob).borrow(bob.address, withFee.add(1)) - ).to.be.revertedWith("PrivatePool: borrower insolvent"); + await expect(mainPair.connect(bob).borrow(bob.address, withFee.add(1))).to.be.revertedWith("PrivatePool: borrower insolvent"); }); it("Should collect the protocol fee immediately", async () => { @@ -531,9 +495,7 @@ describe("Private Lending Pool", async () => { const protocolFeeShare = protocolFee.mul(9).div(20); const takenShare = borrowShare.add(protocolFeeShare); - await expect( - mainPair.connect(bob).borrow(bob.address, borrowAmount) - ).to.emit(mainPair, "LogBorrow"); + await expect(mainPair.connect(bob).borrow(bob.address, borrowAmount)).to.emit(mainPair, "LogBorrow"); const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(assetShare.sub(takenShare)); @@ -545,35 +507,24 @@ describe("Private Lending Pool", async () => { it("Should not lend out more than there is", async () => { // There are 1000 guineas of assets; 150 WETH allows for borrowing // 1350 and is therefore enough: - await mainPair - .connect(bob) - .addCollateral(bob.address, false, getBigNumber(150)); + await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); // More than reserves - await expect( - mainPair.connect(bob).borrow(bob.address, getBigNumber(1001)) - ).to.be.revertedWith("BoringMath: Underflow"); + await expect(mainPair.connect(bob).borrow(bob.address, getBigNumber(1001))).to.be.revertedWith("BoringMath: Underflow"); }); it("Should not defer the protocol fee on new loans", async () => { // This amounts to testing that the amount + protocol fee need to be in // reserve: - await mainPair - .connect(bob) - .addCollateral(bob.address, false, getBigNumber(150)); + await mainPair.connect(bob).addCollateral(bob.address, false, getBigNumber(150)); // Exactly the reserves (our test amounts divide cleanly), but that is // not enough with the fee: const reservesAmount = getBigNumber(1000); - await expect( - mainPair.connect(bob).borrow(bob.address, reservesAmount) - ).to.be.revertedWith("BoringMath: Underflow"); + await expect(mainPair.connect(bob).borrow(bob.address, reservesAmount)).to.be.revertedWith("BoringMath: Underflow"); const cutoff = reservesAmount.mul(1000).div(1001); - await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit( - mainPair, - "LogBorrow" - ); + await expect(mainPair.connect(bob).borrow(bob.address, cutoff)).to.emit(mainPair, "LogBorrow"); }); }); @@ -614,20 +565,13 @@ describe("Private Lending Pool", async () => { await advanceNextTime(YEAR); const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = debtAmount1 - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(YEAR) - .div(getBigNumber(1)); + const extraAmount = debtAmount1.mul(MainTestSettings.INTEREST_PER_SECOND).mul(YEAR).div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()) - .to.emit(mainPair, "LogAccrue") - .withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); // Protocol cut of the open fee + fee on interest, both in shares: - expect((await mainPair.assetBalance()).feesEarnedShare).to.equal( - openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20)) - ); + expect((await mainPair.assetBalance()).feesEarnedShare).to.equal(openFee1.div(10).mul(9).div(20).add(feeAmount.mul(9).div(20))); expect(await mainPair.feesOwedAmount()).to.equal(0); const totalDebt = await mainPair.totalDebt(); @@ -638,17 +582,13 @@ describe("Private Lending Pool", async () => { // all the rounding, this should be roughly 0.07007 times the amount // taken out. Since the contract rounds down, we expect to be under, so // round up for the test: - expect( - extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1) - ).to.equal(7007); + expect(extraAmount.mul(100_000).add(borrowAmount1.sub(1)).div(borrowAmount1)).to.equal(7007); }); it("Should not do anything if nothing is borrowed", async () => { await mainPair.accrue(); // No "LogAccrue" event. Cleaner way to do this? - expect( - await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }]) - ).to.deep.equal([]); + expect(await ethers.provider.send("eth_getLogs", [{ fromBlock: "latest" }])).to.deep.equal([]); }); it("Should defer fees if everything is loaned out", async () => { @@ -658,9 +598,7 @@ describe("Private Lending Pool", async () => { const almostEverything = assetAmount.mul(99).div(100); const openFee = almostEverything.div(1000); const openProtocolFeeShare = openFee.div(10).mul(9).div(20); - const remainingShare = assetShare - .sub(almostEverything.mul(9).div(20)) - .sub(openProtocolFeeShare); + const remainingShare = assetShare.sub(almostEverything.mul(9).div(20)).sub(openProtocolFeeShare); const initialDebt = almostEverything.add(openFee); await mainPair.connect(bob).borrow(bob.address, almostEverything); @@ -670,14 +608,9 @@ describe("Private Lending Pool", async () => { // 7000% interest; the fee is 7% of the initial debt, which is more than // remaining asset reserves. It should still work: const perSecond = MainTestSettings.INTEREST_PER_SECOND; - const extraAmount = initialDebt - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(time) - .div(getBigNumber(1)); + const extraAmount = initialDebt.mul(MainTestSettings.INTEREST_PER_SECOND).mul(time).div(getBigNumber(1)); const feeAmount = extraAmount.div(10); - await expect(mainPair.accrue()) - .to.emit(mainPair, "LogAccrue") - .withArgs(extraAmount, feeAmount); + await expect(mainPair.accrue()).to.emit(mainPair, "LogAccrue").withArgs(extraAmount, feeAmount); // Outstanding debt is recorded normally: const totalDebt = await mainPair.totalDebt(); @@ -688,9 +621,7 @@ describe("Private Lending Pool", async () => { // These already included the protocol fee: const assetBalance = await mainPair.assetBalance(); expect(assetBalance.reservesShare).to.equal(0); - expect(assetBalance.feesEarnedShare).to.equal( - remainingShare.add(openProtocolFeeShare) - ); + expect(assetBalance.feesEarnedShare).to.equal(remainingShare.add(openProtocolFeeShare)); // We collected the remaining asset reserves as fee. The rest is owed: const feeShare = feeAmount.mul(9).div(20); @@ -726,18 +657,12 @@ describe("Private Lending Pool", async () => { // with "seize collateral"-type liquidations; then it's the lender. This // may change if we allow modifying the whitelist; then we'll have to // cleanly handle no-longer-whitelisted users. - expect( - await mainPair.connect(bob).removeCollateral(bob.address, collatShare1) - ) + expect(await mainPair.connect(bob).removeCollateral(bob.address, collatShare1)) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(bob.address, bob.address, collatShare1); const remainder = getBigNumber(12); - expect( - await mainPair - .connect(carol) - .removeCollateral(carol.address, collatShare2.sub(remainder)) - ) + expect(await mainPair.connect(carol).removeCollateral(carol.address, collatShare2.sub(remainder))) .to.emit(mainPair, "LogRemoveCollateral") .withArgs(carol.address, carol.address, collatShare2.sub(remainder)); }); @@ -794,10 +719,7 @@ describe("Private Lending Pool", async () => { expect(await mainPair.borrowerDebtPart(bob.address)).to.equal(debtPart); await advanceNextTime(timeStep); - const extraAmount = debtAmount - .mul(MainTestSettings.INTEREST_PER_SECOND) - .mul(timeStep) - .div(one); + const extraAmount = debtAmount.mul(MainTestSettings.INTEREST_PER_SECOND).mul(timeStep).div(one); debtAmount = debtAmount.add(extraAmount); // "parts" are in units of the initial debt. These should cover it: @@ -806,10 +728,7 @@ describe("Private Lending Pool", async () => { // Bob owns all the debt, so this is the conversion. Rounding is up, in // favour of the contract, so that the amount definitely covers the // part intended to be paid back: - const repayAmount = repayPart - .mul(debtAmount) - .add(debtPart.sub(1)) - .div(debtPart); + const repayAmount = repayPart.mul(debtAmount).add(debtPart.sub(1)).div(debtPart); // "Smallest number of shares covering this" -- so rounded up again: // Note that -- as in the UniV2 AMMs, for instance -- this number of @@ -846,23 +765,15 @@ describe("Private Lending Pool", async () => { const t0 = { bobDebtAmount: bobLoanAmount.mul(1001).div(1000), + bobDebtPart: bobLoanAmount.mul(1001).div(1000), assetBalance: await mainPair.assetBalance(), totalDebt: await mainPair.totalDebt(), }; - t0.bobDebtPart = t0.bobDebtAmount; - expect(t0.assetBalance.reservesShare).to.equal( - assetShare - .sub(bobLoanAmount.mul(9).div(20)) - .sub(bobLoanAmount.div(10_000).mul(9).div(20)) - ); + expect(t0.assetBalance.reservesShare).to.equal(assetShare.sub(bobLoanAmount.mul(9).div(20)).sub(bobLoanAmount.div(10_000).mul(9).div(20))); expect(t0.totalDebt.elastic).to.equal(t0.bobDebtAmount); - const t1 = { - accruedInterest: t0.bobDebtAmount - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), - }; + const t1AccruedInterest = t0.bobDebtAmount.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one); // We find the number of shares Carol should borrow to drain reserves. We // account for the protocol cut of Bob's accrued interest, and Carol's // upcoming opening fee. Rounding up, because the calculation for the fee @@ -871,33 +782,29 @@ describe("Private Lending Pool", async () => { // of wiggle room (here and elsewhere); "multiplication and then division // with rounding" is not an invertible operation, and not every target // can be reached no matter how you round. (Try (M * 2) / 1 == 3). - const carolLoanShare = t0.assetBalance.reservesShare - .sub(t1.accruedInterest.div(10).mul(9).div(20)) - .add(1) - .mul(10_000) - .div(10_001); + const carolLoanShare = t0.assetBalance.reservesShare.sub(t1AccruedInterest.div(10).mul(9).div(20)).add(1).mul(10_000).div(10_001); const carolLoanAmount = carolLoanShare.mul(20).div(9).add(2); await advanceNextTime(timeStep); await mainPair.connect(carol).borrow(carol.address, carolLoanAmount); - t1.assetBalance = await mainPair.assetBalance(); - t1.feesOwedAmount = await mainPair.feesOwedAmount(); + const t1 = { + assetBalance: await mainPair.assetBalance(), + feesOwedAmount: await mainPair.feesOwedAmount(), + totalDebt: await mainPair.totalDebt(), + }; + expect(t1.assetBalance.reservesShare).to.be.lte(1); expect(t1.feesOwedAmount).to.equal(0); // One accrual (after some time) should now be enough to cause fees to // be owed. - t1.totalDebt = await mainPair.totalDebt(); - await advanceNextTime(timeStep); await mainPair.accrue(); const t2 = { - accruedInterest: t1.totalDebt.elastic - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), + accruedInterest: t1.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one), assetBalance: await mainPair.assetBalance(), totalDebt: await mainPair.totalDebt(), feesOwedAmount: await mainPair.feesOwedAmount(), @@ -908,22 +815,14 @@ describe("Private Lending Pool", async () => { // the error is more than 1 or even `toAmount(1)`: expect(t2.assetBalance.reservesShare).to.equal(0); expect(t2.feesOwedAmount).to.be.gt(0); - expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte( - 5 - ); + expect(t2.feesOwedAmount.sub(t2.accruedInterest.div(10)).abs()).to.be.lte(5); // Repaying triggers another accrual, so we (advance a fixed time and) // determine how much will be owed after that: - const t3 = { - accruedInterest: t2.totalDebt.elastic - .mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)) - .div(one), - }; + const t3AccruedInterest = t2.totalDebt.elastic.mul(MainTestSettings.INTEREST_PER_SECOND.mul(timeStep)).div(one); // Intermediate value; we would see it if we did something that accrues // but not deposits any assets: - t3.feesOwedBeforeRepay = t2.feesOwedAmount.add( - t3.accruedInterest.div(10) - ); + const t3FeesOwedBeforeRepay = t2.feesOwedAmount.add(t3AccruedInterest.div(10)); // A debt "part" corresponds to 1 token when the first loan is taken out. // When interest accrues this amount grows correspondingly; it never @@ -932,26 +831,24 @@ describe("Private Lending Pool", async () => { // we've seen a bit over 1 week of 7%-a-year interest at this point. // The amount we want to repay is the intermediate value of fees owed; // after the accrual that gets triggered - const repayPart = t3.feesOwedBeforeRepay; + const repayPart = t3FeesOwedBeforeRepay; await advanceNextTime(timeStep); await mainPair.connect(bob).repay(bob.address, false, repayPart); - t3.assetBalance = await mainPair.assetBalance(); - t3.totalDebt = await mainPair.totalDebt(); - t3.bobDebtPart = await mainPair.borrowerDebtPart(bob.address); - t3.feesOwedAmount = await mainPair.feesOwedAmount(); + const t3 = { + assetBalance: await mainPair.assetBalance(), + totalDebt: await mainPair.totalDebt(), + bobDebtPart: await mainPair.borrowerDebtPart(bob.address), + feesOwedAmount: await mainPair.feesOwedAmount(), + }; // Before the accrual, there were already fees owed, and therefore no // asset reserves. More fees were then incurred. Since they could not be // taken out of reserves, they were not "earned" until we made the // repayment. At which point we expect "fees earned" to have increased // by exactly that amount (in shares): - expect(t3.assetBalance.feesEarnedShare).to.equal( - t2.assetBalance.feesEarnedShare.add( - t3.feesOwedBeforeRepay.mul(9).div(20) - ) - ); + expect(t3.assetBalance.feesEarnedShare).to.equal(t2.assetBalance.feesEarnedShare.add(t3FeesOwedBeforeRepay.mul(9).div(20))); expect(t3.feesOwedAmount).to.equal(0); // Debt gets paid off as normal; in particular the fees are not added to @@ -961,16 +858,11 @@ describe("Private Lending Pool", async () => { // The small amount we repaid in excess of the fees owed should have gone // to asset reserves. - const excessRepayAmount = repayPart - .mul(t3.totalDebt.elastic) - .div(t3.totalDebt.base) - .sub(t3.feesOwedBeforeRepay); + const excessRepayAmount = repayPart.mul(t3.totalDebt.elastic).div(t3.totalDebt.base).sub(t3FeesOwedBeforeRepay); const excessRepayShare = excessRepayAmount.mul(9).div(20); // Again, giving it a few wei of leeway due to rounding in _receiveAsset: - expect( - t3.assetBalance.reservesShare.sub(excessRepayShare).abs() - ).to.be.lte(5); + expect(t3.assetBalance.reservesShare.sub(excessRepayShare).abs()).to.be.lte(5); }); }); @@ -989,15 +881,33 @@ describe("Private Lending Pool", async () => { const bobLoanAmount = getBigNumber(100); // ~50% LTV const carolLoanAmount = getBigNumber(100); // ~33% LTV - const t0 = {}; + let t0; + const getState = async () => { + const [a, b, c] = [alice, bob, carol].map((x) => x.address); + + return { + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, a), + aliceBentoWeth: await bentoBox.balanceOf(weth.address, a), + + bobCollateralShare: await mainPair.userCollateralShare(b), + carolCollateralShare: await mainPair.userCollateralShare(c), + + bobDebtPart: await mainPair.borrowerDebtPart(b), + carolDebtPart: await mainPair.borrowerDebtPart(c), + + totalDebt: await mainPair.totalDebt(), + assetBalance: await mainPair.assetBalance(), + collateralBalance: await mainPair.collateralBalance(), + + blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, + }; + }; before(async () => { const [a, b, c] = [alice, bob, carol].map((x) => x.address); await mainPair.connect(alice).addAsset(false, assetShare); await mainPair.connect(bob).addCollateral(b, false, bobCollateralShare); - await mainPair - .connect(carol) - .addCollateral(c, false, carolCollateralShare); + await mainPair.connect(carol).addCollateral(c, false, carolCollateralShare); await oracle.set(one.div(10)); // one WETH is 10 guineas await mainPair.updateExchangeRate(); @@ -1005,33 +915,15 @@ describe("Private Lending Pool", async () => { await mainPair.connect(bob).borrow(b, bobLoanAmount); await mainPair.connect(carol).borrow(c, carolLoanAmount); - t0.aliceBentoGuineas = await bentoBox.balanceOf(guineas.address, a); - t0.aliceBentoWeth = await bentoBox.balanceOf(weth.address, a); - - t0.bobCollateralShare = await mainPair.userCollateralShare(b); - t0.carolCollateralShare = await mainPair.userCollateralShare(c); - - t0.bobDebtPart = await mainPair.borrowerDebtPart(b); - t0.carolDebtPart = await mainPair.borrowerDebtPart(c); - - t0.totalDebt = await mainPair.totalDebt(); - t0.assetBalance = await mainPair.assetBalance(); - t0.collateralBalance = await mainPair.collateralBalance(); - - t0.blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + t0 = await getState(); }); it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect( - mainPair.liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.be.revertedWith("PrivatePool: all are solvent"); + await expect(mainPair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( + "PrivatePool: all are solvent" + ); }); it("Should liquidate insolvent borrowers only", async () => { @@ -1046,16 +938,7 @@ describe("Private Lending Pool", async () => { // Alice has guineas and has approved the contract. That she is also the // lender makes no difference in the execution path taken. - await expect( - mainPair - .connect(alice) - .liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ) + await expect(mainPair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)) .to.emit(mainPair, "LogRemoveCollateral") .to.emit(mainPair, "LogRepay"); @@ -1070,10 +953,7 @@ describe("Private Lending Pool", async () => { assetBalance: await mainPair.assetBalance(), - aliceBentoGuineas: await bentoBox.balanceOf( - guineas.address, - alice.address - ), + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -1098,45 +978,28 @@ describe("Private Lending Pool", async () => { const minRepayShare = bobLiquidatePart.mul(9).div(20); // Not entirely accurate because it gets calculated differently, but // equivalent up to rounding effects: - const protocolFeeShare = minRepayShare - .mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) - .div(10_000) - .sub(minRepayShare) - .div(10); - const aliceMaxBentoGuineas = t0.aliceBentoGuineas - .sub(minRepayShare) - .sub(protocolFeeShare); + const protocolFeeShare = minRepayShare.mul(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS).div(10_000).sub(minRepayShare).div(10); + const aliceMaxBentoGuineas = t0.aliceBentoGuineas.sub(minRepayShare).sub(protocolFeeShare); expect(t1.aliceBentoGuineas).to.be.lte(aliceMaxBentoGuineas); - expect(t1.aliceBentoGuineas).to.be.gte( - aliceMaxBentoGuineas.mul(9999).div(10_000) - ); + expect(t1.aliceBentoGuineas).to.be.gte(aliceMaxBentoGuineas.mul(9999).div(10_000)); - const aliceMinBentoWeth = t0.aliceBentoWeth.add( - bobMinCollateralTakenShare - ); + const aliceMinBentoWeth = t0.aliceBentoWeth.add(bobMinCollateralTakenShare); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte( - aliceMinBentoWeth.mul(10_001).div(10_000) - ); + expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); // If we want a firm lower bound on asset reserves, we need to account // for interest: the accrue() call right before liquidations charges // interest, and takes the protocol cut of that interest out of reserves. // We divide rounding up: - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( - t1.blockTimestamp - t0.blockTimestamp - ) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = t0.assetBalance.reservesShare - .add(minRepayShare) - .sub(maxInterestFee); - const minFeesEarnedShare = - t0.assetBalance.feesEarnedShare.add(protocolFeeShare); + const minAssetReserves = t0.assetBalance.reservesShare.add(minRepayShare).sub(maxInterestFee); + const minFeesEarnedShare = t0.assetBalance.feesEarnedShare.add(protocolFeeShare); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); expect(t1.assetBalance.feesEarnedShare).to.be.gte(minFeesEarnedShare); @@ -1146,21 +1009,13 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal( - t0.totalDebt.base.sub(bobLiquidatePart) - ); + expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); - const bobMaxCollateralShare = t0.bobCollateralShare.sub( - bobMinCollateralTakenShare - ); + const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte( - bobMaxCollateralShare.mul(9999).div(10_000) - ); + expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); // Equivalent check.. - expect(t1.collateralBalance.userTotalShare).to.equal( - t1.bobCollateralShare.add(t1.carolCollateralShare) - ); + expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare)); }); }); @@ -1179,9 +1034,30 @@ describe("Private Lending Pool", async () => { const bobLoanAmount = getBigNumber(100); // ~50% LTV const carolLoanAmount = getBigNumber(100); // ~33% LTV - const t0 = {}; let pair; + let t0; + const getState = async () => { + const [a, b, c] = [alice, bob, carol].map((x) => x.address); + + return { + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, a), + aliceBentoWeth: await bentoBox.balanceOf(weth.address, a), + + bobCollateralShare: await pair.userCollateralShare(b), + carolCollateralShare: await pair.userCollateralShare(c), + + bobDebtPart: await pair.borrowerDebtPart(b), + carolDebtPart: await pair.borrowerDebtPart(c), + + totalDebt: await pair.totalDebt(), + assetBalance: await pair.assetBalance(), + collateralBalance: await pair.collateralBalance(), + + blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, + }; + }; + before(async () => { const [a, b, c] = [alice, bob, carol].map((x) => x.address); @@ -1205,33 +1081,15 @@ describe("Private Lending Pool", async () => { await pair.connect(bob).borrow(b, bobLoanAmount); await pair.connect(carol).borrow(c, carolLoanAmount); - t0.aliceBentoGuineas = await bentoBox.balanceOf(guineas.address, a); - t0.aliceBentoWeth = await bentoBox.balanceOf(weth.address, a); - - t0.bobCollateralShare = await pair.userCollateralShare(b); - t0.carolCollateralShare = await pair.userCollateralShare(c); - - t0.bobDebtPart = await pair.borrowerDebtPart(b); - t0.carolDebtPart = await pair.borrowerDebtPart(c); - - t0.totalDebt = await pair.totalDebt(); - t0.assetBalance = await pair.assetBalance(); - t0.collateralBalance = await pair.collateralBalance(); - - t0.blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + t0 = await getState(); }); it("Should refuse to liquidate solvent borrowers, at all", async () => { // Not enough time will have passed to make either borrower insolvent // over the accrued interest: - await expect( - pair.liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.be.revertedWith("PrivatePool: all are solvent"); + await expect(pair.liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.be.revertedWith( + "PrivatePool: all are solvent" + ); }); it("Should liquidate insolvent borrowers only", async () => { @@ -1246,16 +1104,10 @@ describe("Private Lending Pool", async () => { // That Alice she is also the lender makes no difference in the execution // path taken. - await expect( - pair - .connect(alice) - .liquidate( - [bob.address, carol.address], - [one, one], - alice.address, - AddressZero - ) - ).to.emit(pair, "LogSeizeCollateral"); + await expect(pair.connect(alice).liquidate([bob.address, carol.address], [one, one], alice.address, AddressZero)).to.emit( + pair, + "LogSeizeCollateral" + ); const t1 = { totalDebt: await pair.totalDebt(), @@ -1269,10 +1121,7 @@ describe("Private Lending Pool", async () => { assetBalance: await pair.assetBalance(), collateralBalance: await pair.collateralBalance(), - aliceBentoGuineas: await bentoBox.balanceOf( - guineas.address, - alice.address - ), + aliceBentoGuineas: await bentoBox.balanceOf(guineas.address, alice.address), aliceBentoWeth: await bentoBox.balanceOf(weth.address, alice.address), blockTimestamp: (await ethers.provider.getBlock("latest")).timestamp, @@ -1306,12 +1155,8 @@ describe("Private Lending Pool", async () => { .div(MainTestSettings.LIQUIDATION_MULTIPLIER_BPS) .add(minCollateralLiquidatorShare); - expect(t1.collateralBalance.feesEarnedShare).to.be.gte( - minCollateralFeeShare - ); - expect(t1.collateralBalance.feesEarnedShare).to.be.lte( - minCollateralFeeShare.mul(10_001).div(10_000) - ); + expect(t1.collateralBalance.feesEarnedShare).to.be.gte(minCollateralFeeShare); + expect(t1.collateralBalance.feesEarnedShare).to.be.lte(minCollateralFeeShare.mul(10_001).div(10_000)); // Alice gets the liquidator part of the bonus only, in kind, minus the // protocol fee. The contract gets the protocol fee over the bonus. @@ -1319,31 +1164,22 @@ describe("Private Lending Pool", async () => { expect(t1.aliceBentoGuineas).to.equal(t0.aliceBentoGuineas); // Alice the liquidator: - const aliceMinBentoWeth = t0.aliceBentoWeth.add( - minCollateralLiquidatorShare - ); + const aliceMinBentoWeth = t0.aliceBentoWeth.add(minCollateralLiquidatorShare); expect(t1.aliceBentoWeth).to.be.gte(aliceMinBentoWeth); - expect(t1.aliceBentoWeth).to.be.lte( - aliceMinBentoWeth.mul(10_001).div(10_000) - ); + expect(t1.aliceBentoWeth).to.be.lte(aliceMinBentoWeth.mul(10_001).div(10_000)); // Alice the lender: expect(t1.aliceCollateralShare).to.be.gte(minCollateralLenderShare); - expect(t1.aliceCollateralShare).to.be.lte( - minCollateralLenderShare.mul(10_001).div(10_000) - ); + expect(t1.aliceCollateralShare).to.be.lte(minCollateralLenderShare.mul(10_001).div(10_000)); // Asset reserves do not really change, except for the interest fee.. - const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul( - t1.blockTimestamp - t0.blockTimestamp - ) + const maxInterestFee = MainTestSettings.INTEREST_PER_SECOND.mul(t1.blockTimestamp - t0.blockTimestamp) .mul(t0.totalDebt.elastic) .add(one.sub(1)) .div(one) .add(9) .div(10); - const minAssetReserves = - t0.assetBalance.reservesShare.sub(maxInterestFee); + const minAssetReserves = t0.assetBalance.reservesShare.sub(maxInterestFee); expect(t1.assetBalance.reservesShare).to.be.gte(minAssetReserves); // Carol was not insolvent, so that liquidation failed: @@ -1352,23 +1188,13 @@ describe("Private Lending Pool", async () => { // Bob got liquidated; this affects his balance and the totals: expect(t1.bobDebtPart).to.equal(t0.bobDebtPart.sub(bobLiquidatePart)); - expect(t1.totalDebt.base).to.equal( - t0.totalDebt.base.sub(bobLiquidatePart) - ); + expect(t1.totalDebt.base).to.equal(t0.totalDebt.base.sub(bobLiquidatePart)); - const bobMaxCollateralShare = t0.bobCollateralShare.sub( - bobMinCollateralTakenShare - ); + const bobMaxCollateralShare = t0.bobCollateralShare.sub(bobMinCollateralTakenShare); expect(t1.bobCollateralShare).to.be.lte(bobMaxCollateralShare); - expect(t1.bobCollateralShare).to.be.gte( - bobMaxCollateralShare.mul(9999).div(10_000) - ); + expect(t1.bobCollateralShare).to.be.gte(bobMaxCollateralShare.mul(9999).div(10_000)); // Given that individual shares are as expected, this tests the total: - expect(t1.collateralBalance.userTotalShare).to.equal( - t1.bobCollateralShare - .add(t1.carolCollateralShare) - .add(t1.aliceCollateralShare) - ); + expect(t1.collateralBalance.userTotalShare).to.equal(t1.bobCollateralShare.add(t1.carolCollateralShare).add(t1.aliceCollateralShare)); }); }); @@ -1398,24 +1224,12 @@ describe("Private Lending Pool", async () => { await coinPair.updateExchangeRate(); await dollar.connect(alice).approve(bentoBox.address, MaxUint256); - await bentoBox - .connect(alice) - .deposit( - dollar.address, - alice.address, - alice.address, - totalSupply.div(10), - 0 - ); + await bentoBox.connect(alice).deposit(dollar.address, alice.address, alice.address, totalSupply.div(10), 0); await coinPair.connect(alice).addAsset(false, totalSupply.div(10)); await coin.connect(bob).approve(bentoBox.address, MaxUint256); - await bentoBox - .connect(bob) - .deposit(coin.address, bob.address, bob.address, totalSupply, 0); - await coinPair - .connect(bob) - .addCollateral(bob.address, false, totalSupply); + await bentoBox.connect(bob).deposit(coin.address, bob.address, bob.address, totalSupply, 0); + await coinPair.connect(bob).addCollateral(bob.address, false, totalSupply); const assetBalance = await coinPair.assetBalance(); const collateralBalance = await coinPair.collateralBalance(); @@ -1429,25 +1243,15 @@ describe("Private Lending Pool", async () => { expect(bentoCoinTotals.elastic).to.equal(totalSupply); if (shouldPass) { - await expect(coinPair.connect(bob).borrow(bob.address, 1)) - .to.emit(coinPair, "LogBorrow") - .to.emit(bentoBox, "LogTransfer"); + await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.emit(coinPair, "LogBorrow").to.emit(bentoBox, "LogTransfer"); } else { - await expect( - coinPair.connect(bob).borrow(bob.address, 1) - ).to.be.revertedWith("BoringMath: Mul Overflow"); + await expect(coinPair.connect(bob).borrow(bob.address, 1)).to.be.revertedWith("BoringMath: Mul Overflow"); } }; - it( - "Works with all SPELL as collateral (no strat losses)", - makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true) - ); + it("Works with all SPELL as collateral (no strat losses)", makeIsSolventTest(getBigNumber(210_000_000_000n, 18), true)); // This fails with the old Kashi-style `isSolvent`: - it( - "No longer breaks at 393 billion (18-decimal) tokens", - makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true) - ); + it("No longer breaks at 393 billion (18-decimal) tokens", makeIsSolventTest(getBigNumber(393_000_000_000n, 18), true)); }); }); From e1cf116ca974a24c47dde6e2f2c463f3db3017cd Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Thu, 30 Jun 2022 06:23:29 +0200 Subject: [PATCH 104/107] Still archive the PrivatePool tests --- {test => archive/test}/PrivatePool.forking.test.ts | 0 {test => archive/test}/PrivatePool.test.ts | 0 {test => archive/test}/PrivatePool.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {test => archive/test}/PrivatePool.forking.test.ts (100%) rename {test => archive/test}/PrivatePool.test.ts (100%) rename {test => archive/test}/PrivatePool.ts (100%) diff --git a/test/PrivatePool.forking.test.ts b/archive/test/PrivatePool.forking.test.ts similarity index 100% rename from test/PrivatePool.forking.test.ts rename to archive/test/PrivatePool.forking.test.ts diff --git a/test/PrivatePool.test.ts b/archive/test/PrivatePool.test.ts similarity index 100% rename from test/PrivatePool.test.ts rename to archive/test/PrivatePool.test.ts diff --git a/test/PrivatePool.ts b/archive/test/PrivatePool.ts similarity index 100% rename from test/PrivatePool.ts rename to archive/test/PrivatePool.ts From 1a9d1662bd73572d446915490a679f5ed51e49b7 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Sun, 3 Jul 2022 23:57:44 +0200 Subject: [PATCH 105/107] Change ILendingClub interface to work with either contract --- contracts/NFTPair.sol | 13 +++++++++++- contracts/NFTPairWithOracle.sol | 14 +++++++++++-- contracts/interfaces/ILendingClub.sol | 13 +++++++++--- .../interfaces/ILendingClubWithOracle.sol | 20 ------------------- contracts/interfaces/INFTPair.sol | 8 +------- contracts/interfaces/INFTPairWithOracle.sol | 11 +--------- contracts/interfaces/TokenLoanParams.sol | 8 ++++++++ .../interfaces/TokenLoanParamsWithOracle.sol | 12 +++++++++++ 8 files changed, 56 insertions(+), 43 deletions(-) delete mode 100644 contracts/interfaces/ILendingClubWithOracle.sol create mode 100644 contracts/interfaces/TokenLoanParams.sol create mode 100644 contracts/interfaces/TokenLoanParamsWithOracle.sol diff --git a/contracts/NFTPair.sol b/contracts/NFTPair.sol index c602e964..8b232ae1 100644 --- a/contracts/NFTPair.sol +++ b/contracts/NFTPair.sol @@ -438,7 +438,18 @@ contract NFTPair is BoringOwnable, Domain, IMasterContract { SignatureParams memory signature ) private { if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClub(lender).willLend(tokenId, params), "NFTPair: LendingClub refused"); + require( + ILendingClub(lender).willLend( + tokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + // Oracle-specific values, not relevant here: + 0, + address(0) + ), + "NFTPair: LendingClub refused" + ); } else { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; diff --git a/contracts/NFTPairWithOracle.sol b/contracts/NFTPairWithOracle.sol index 54b3b7f3..3219677c 100644 --- a/contracts/NFTPairWithOracle.sol +++ b/contracts/NFTPairWithOracle.sol @@ -26,7 +26,7 @@ import "@boringcrypto/boring-solidity/contracts/interfaces/IMasterContract.sol"; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./interfaces/IERC721.sol"; -import "./interfaces/ILendingClubWithOracle.sol"; +import "./interfaces/ILendingClub.sol"; import "./interfaces/INFTBuyer.sol"; import "./interfaces/INFTSeller.sol"; import "./interfaces/INFTPairWithOracle.sol"; @@ -463,7 +463,17 @@ contract NFTPairWithOracle is BoringOwnable, Domain, IMasterContract { SignatureParams memory signature ) private { if (signature.v == 0 && signature.r == bytes32(0) && signature.s == bytes32(0)) { - require(ILendingClubWithOracle(lender).willLend(tokenId, params), "NFTPair: LendingClub refused"); + require( + ILendingClub(lender).willLend( + tokenId, + params.valuation, + params.duration, + params.annualInterestBPS, + params.ltvBPS, + address(params.oracle) + ), + "NFTPair: LendingClub refused" + ); } else { require(block.timestamp <= signature.deadline, "NFTPair: signature expired"); uint256 nonce = nonces[lender]++; diff --git a/contracts/interfaces/ILendingClub.sol b/contracts/interfaces/ILendingClub.sol index b1a0b936..8c895977 100644 --- a/contracts/interfaces/ILendingClub.sol +++ b/contracts/interfaces/ILendingClub.sol @@ -3,11 +3,18 @@ pragma solidity >=0.6.12 <0.9.0; pragma experimental ABIEncoderV2; -import "./INFTPair.sol"; +import "./TokenLoanParamsWithOracle.sol"; interface ILendingClub { // Per token settings. - function willLend(uint256 tokenId, TokenLoanParams memory params) + function willLend( + uint256 tokenId, + uint128 valuation, + uint64 duration, + uint16 annualInterestBPS, + uint16 ltvBPS, + address oracle + ) external view returns (bool); @@ -15,6 +22,6 @@ interface ILendingClub { function lendingConditions(address nftPair, uint256 tokenId) external view - returns (TokenLoanParams memory); + returns (TokenLoanParamsWithOracle[] memory); } diff --git a/contracts/interfaces/ILendingClubWithOracle.sol b/contracts/interfaces/ILendingClubWithOracle.sol deleted file mode 100644 index 73168ba5..00000000 --- a/contracts/interfaces/ILendingClubWithOracle.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity >=0.6.12 <0.9.0; -pragma experimental ABIEncoderV2; - -import "./INFTPairWithOracle.sol"; - -interface ILendingClubWithOracle { - // Per token settings. - function willLend(uint256 tokenId, TokenLoanParamsWithOracle memory params) - external - view - returns (bool); - - function lendingConditions(address nftPair, uint256 tokenId) - external - view - returns (TokenLoanParamsWithOracle memory); -} - diff --git a/contracts/interfaces/INFTPair.sol b/contracts/interfaces/INFTPair.sol index 782fbbd2..45905266 100644 --- a/contracts/interfaces/INFTPair.sol +++ b/contracts/interfaces/INFTPair.sol @@ -6,6 +6,7 @@ import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./IERC721.sol"; import "./SignatureParams.sol"; +import "./TokenLoanParams.sol"; interface INFTPair { function collateral() external view returns (IERC721); @@ -18,10 +19,3 @@ interface INFTPair { function removeCollateral(uint256 tokenId, address to) external; } - -struct TokenLoanParams { - uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 duration; // Length of loan in seconds - uint16 annualInterestBPS; // Variable cost of taking out the loan -} - diff --git a/contracts/interfaces/INFTPairWithOracle.sol b/contracts/interfaces/INFTPairWithOracle.sol index 85e208ba..2da08cd5 100644 --- a/contracts/interfaces/INFTPairWithOracle.sol +++ b/contracts/interfaces/INFTPairWithOracle.sol @@ -5,8 +5,8 @@ pragma solidity >=0.6.12 <0.9.0; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "@sushiswap/bentobox-sdk/contracts/IBentoBoxV1.sol"; import "./IERC721.sol"; -import "./INFTOracle.sol"; import "./SignatureParams.sol"; +import "./TokenLoanParamsWithOracle.sol"; // TODO: Add more methods for LendingClubWitOracle integration.. interface INFTPairWithOracle { @@ -20,12 +20,3 @@ interface INFTPairWithOracle { function removeCollateral(uint256 tokenId, address to) external; } - -struct TokenLoanParamsWithOracle { - uint128 valuation; // How much will you get? OK to owe until expiration. - uint64 duration; // Length of loan in seconds - uint16 annualInterestBPS; // Variable cost of taking out the loan - uint16 ltvBPS; // Required to avoid liquidation - INFTOracle oracle; // Oracle used for price -} - diff --git a/contracts/interfaces/TokenLoanParams.sol b/contracts/interfaces/TokenLoanParams.sol new file mode 100644 index 00000000..0ff1a9dd --- /dev/null +++ b/contracts/interfaces/TokenLoanParams.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12 <0.9.0; + +struct TokenLoanParams { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 annualInterestBPS; // Variable cost of taking out the loan +} diff --git a/contracts/interfaces/TokenLoanParamsWithOracle.sol b/contracts/interfaces/TokenLoanParamsWithOracle.sol new file mode 100644 index 00000000..4e5d1b49 --- /dev/null +++ b/contracts/interfaces/TokenLoanParamsWithOracle.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12 <0.9.0; + +import "./INFTOracle.sol"; + +struct TokenLoanParamsWithOracle { + uint128 valuation; // How much will you get? OK to owe until expiration. + uint64 duration; // Length of loan in seconds + uint16 annualInterestBPS; // Variable cost of taking out the loan + uint16 ltvBPS; // Required to avoid liquidation + INFTOracle oracle; // Oracle used for price +} From 6ec5a2ef44a7a03e58cb1f33baaeaa14de4c2178 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Tue, 30 Aug 2022 20:57:47 +0200 Subject: [PATCH 106/107] Start NFT price floor example --- contracts/mocks/LendingClubMock.sol | 36 ++++++++----- contracts/nft-examples/NFTPriceFloor.sol | 69 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 contracts/nft-examples/NFTPriceFloor.sol diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index 0965e5ee..a95a11ae 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -4,6 +4,7 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; import "../interfaces/INFTPair.sol"; +import "../interfaces/TokenLoanParamsWithOracle.sol"; // Minimal implementation to set up some tests. contract LendingClubMock { @@ -19,40 +20,47 @@ contract LendingClubMock { nftPair.bentoBox().setMasterContractApproval(address(this), address(nftPair.masterContract()), true, 0, bytes32(0), bytes32(0)); } - function willLend(uint256 tokenId, TokenLoanParams memory requested) external view returns (bool) { + function willLend( + uint256 tokenId, + uint128 valuation, + uint64 duration, + uint16 annualInterestBPS, + uint16 _ltvBPS, + address _oracle + ) external view returns (bool) { if (msg.sender != address(nftPair)) { return false; } - TokenLoanParams memory accepted = _lendingConditions(tokenId); + TokenLoanParamsWithOracle memory accepted = _lendingConditions(tokenId)[0]; // Valuation has to be an exact match, everything else must be at least // as good for the lender as `accepted`. - return - requested.valuation == accepted.valuation && - requested.duration <= accepted.duration && - requested.annualInterestBPS >= accepted.annualInterestBPS; + return valuation == accepted.valuation && duration <= accepted.duration && annualInterestBPS >= accepted.annualInterestBPS; } - function _lendingConditions(uint256 tokenId) private pure returns (TokenLoanParams memory) { - TokenLoanParams memory conditions; + function _lendingConditions(uint256 tokenId) private pure returns (TokenLoanParamsWithOracle[] memory) { // No specific conditions given, but we'll take all even-numbered // ones at 100% APY: if (tokenId % 2 == 0) { + TokenLoanParamsWithOracle[] memory conditions = new TokenLoanParamsWithOracle[](1); // 256-bit addition fits by the above check. // Cast is.. relatively safe: this is a mock implementation, // production use is unlikely to follow this pattern for valuing // loans, and manipulating the token ID can only break the logic by // making the loan "safer" for the lender. - conditions.valuation = uint128((tokenId + 1) * 10**18); - conditions.duration = 365 days; - conditions.annualInterestBPS = 10_000; + conditions[0].valuation = uint128((tokenId + 1) * 10**18); + conditions[0].duration = 365 days; + conditions[0].annualInterestBPS = 10_000; + return conditions; + } else { + TokenLoanParamsWithOracle[] memory conditions; + return conditions; } - return conditions; } - function lendingConditions(address _nftPair, uint256 tokenId) external view returns (TokenLoanParams memory) { + function lendingConditions(address _nftPair, uint256 tokenId) external view returns (TokenLoanParamsWithOracle[] memory) { if (_nftPair != address(nftPair)) { - TokenLoanParams memory empty; + TokenLoanParamsWithOracle[] memory empty; return empty; } else { return _lendingConditions(tokenId); diff --git a/contracts/nft-examples/NFTPriceFloor.sol b/contracts/nft-examples/NFTPriceFloor.sol new file mode 100644 index 00000000..dc415fae --- /dev/null +++ b/contracts/nft-examples/NFTPriceFloor.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; +import "@boringcrypto/boring-solidity/contracts/libraries/BoringERC20.sol"; +import "@boringcrypto/boring-solidity/contracts/BoringOwnable.sol"; +import "../interfaces/INFTPair.sol"; +import "../interfaces/ILendingClub.sol"; +import "../interfaces/TokenLoanParams.sol"; + +// Not really a min price; if you "sell" to this club you get an option. + +contract NFTPriceFloor is BoringOwnable, ILendingClub { + mapping(uint256 => TokenLoanParams) private loanParams; + INFTPair private immutable pair; + + constructor(INFTPair nftPair) public { + pair = nftPair; + } + + /// @param tokenId The token ID of the loan in question + /// @param params The loan parameters to be offered + function offer(uint256 tokenId, TokenLoanParams memory params) external onlyOwner { + loanParams[tokenId] = params; + } + + function willLend( + uint256 tokenId, + uint128 valuation, + uint64 duration, + uint16 annualInterestBPS, + uint16 _ltvBPS, + address oracle + ) external view override returns (bool) { + if (msg.sender != address(pair)) { + return false; + } + TokenLoanParams memory params = loanParams[tokenId]; + if (valuation > params.valuation) { + return false; + } + if (duration > params.duration) { + return false; + } + if (annualInterestBPS < params.annualInterestBPS) { + return false; + } + if (oracle != address(0)) { + return false; + } + // (If we don't have the funds, just let the transaction revert) + + return true; + } + + function lendingConditions(address nftPair, uint256 tokenId) external view override returns (TokenLoanParamsWithOracle[] memory) { + TokenLoanParams memory params = loanParams[tokenId]; + if (nftPair != address(pair) || params.valuation == 0) { + TokenLoanParamsWithOracle[] memory empty; + return empty; + } + // TODO: Cast? + TokenLoanParamsWithOracle[] memory conditions = new TokenLoanParamsWithOracle[](1); + conditions[0].valuation = params.valuation; + conditions[0].duration = params.duration; + conditions[0].annualInterestBPS = params.annualInterestBPS; + return conditions; + } +} From 96aa80f505530561000ec344abca07ff8b1aa9a4 Mon Sep 17 00:00:00 2001 From: Barry Lyndon Date: Wed, 21 Sep 2022 03:16:55 +0200 Subject: [PATCH 107/107] (Fix lending pool mock) --- contracts/mocks/LendingClubMock.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/LendingClubMock.sol b/contracts/mocks/LendingClubMock.sol index a95a11ae..88155649 100644 --- a/contracts/mocks/LendingClubMock.sol +++ b/contracts/mocks/LendingClubMock.sol @@ -31,7 +31,11 @@ contract LendingClubMock { if (msg.sender != address(nftPair)) { return false; } - TokenLoanParamsWithOracle memory accepted = _lendingConditions(tokenId)[0]; + TokenLoanParamsWithOracle[] memory options = _lendingConditions(tokenId); + if (options.length == 0) { + return false; + } + TokenLoanParamsWithOracle memory accepted = options[0]; // Valuation has to be an exact match, everything else must be at least // as good for the lender as `accepted`.