diff --git a/contracts/BorrowController.sol b/contracts/BorrowController.sol index c9c701a..26e743c 100644 --- a/contracts/BorrowController.sol +++ b/contracts/BorrowController.sol @@ -90,7 +90,7 @@ contract BorrowController is IBorrowController, Controller, IPositionLocker { ); if (collateralDelta > 0) { - ISubstitute(position.collateralToken).ensureBalance(user, uint256(collateralDelta)); + _mintSubstituteAll(position.collateralToken, user, uint256(collateralDelta)); IERC20(position.collateralToken).approve(address(_loanPositionManager), uint256(collateralDelta)); _loanPositionManager.depositToken(position.collateralToken, uint256(collateralDelta)); } diff --git a/contracts/BorrowControllerV2.sol b/contracts/BorrowControllerV2.sol new file mode 100644 index 0000000..6b8b52e --- /dev/null +++ b/contracts/BorrowControllerV2.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: - +// License: https://license.coupon.finance/LICENSE.pdf + +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IBorrowControllerV2} from "./interfaces/IBorrowControllerV2.sol"; +import {ILoanPositionManager} from "./interfaces/ILoanPositionManager.sol"; +import {ISubstitute} from "./interfaces/ISubstitute.sol"; +import {IPositionLocker} from "./interfaces/IPositionLocker.sol"; +import {LoanPosition} from "./libraries/LoanPosition.sol"; +import {Coupon} from "./libraries/Coupon.sol"; +import {Epoch, EpochLibrary} from "./libraries/Epoch.sol"; +import {ControllerV2} from "./libraries/ControllerV2.sol"; +import {ERC20PermitParams, PermitSignature, PermitParamsLibrary} from "./libraries/PermitParams.sol"; + +contract BorrowControllerV2 is IBorrowControllerV2, ControllerV2, IPositionLocker { + using PermitParamsLibrary for *; + using EpochLibrary for Epoch; + + ILoanPositionManager private immutable _loanPositionManager; + address private immutable _router; + + modifier onlyPositionOwner(uint256 positionId) { + if (_loanPositionManager.ownerOf(positionId) != msg.sender) revert InvalidAccess(); + _; + } + + constructor( + address wrapped1155Factory, + address cloberController, + address bookManager, + address couponManager, + address weth, + address loanPositionManager, + address router + ) ControllerV2(wrapped1155Factory, cloberController, bookManager, couponManager, weth) { + _loanPositionManager = ILoanPositionManager(loanPositionManager); + _router = router; + } + + function positionLockAcquired(bytes memory data) external returns (bytes memory result) { + if (msg.sender != address(_loanPositionManager)) revert InvalidAccess(); + + uint256 positionId; + address user; + SwapParams memory swapParams; + (positionId, user, swapParams, data) = abi.decode(data, (uint256, address, SwapParams, bytes)); + if (positionId == 0) { + address collateralToken; + address debtToken; + (collateralToken, debtToken, data) = abi.decode(data, (address, address, bytes)); + positionId = _loanPositionManager.mint(collateralToken, debtToken); + result = abi.encode(positionId); + } + LoanPosition memory position = _loanPositionManager.getPosition(positionId); + + int256 interestThreshold; + (position.collateralAmount, position.debtAmount, position.expiredWith, interestThreshold) = + abi.decode(data, (uint256, uint256, Epoch, int256)); + + (Coupon[] memory couponsToMint, Coupon[] memory couponsToBurn, int256 collateralDelta, int256 debtDelta) = + _loanPositionManager.adjustPosition( + positionId, position.collateralAmount, position.debtAmount, position.expiredWith + ); + if (collateralDelta < 0) { + _loanPositionManager.withdrawToken(position.collateralToken, address(this), uint256(-collateralDelta)); + } + if (debtDelta > 0) _loanPositionManager.withdrawToken(position.debtToken, address(this), uint256(debtDelta)); + if (couponsToMint.length > 0) { + _loanPositionManager.mintCoupons(couponsToMint, address(this), ""); + _wrapCoupons(couponsToMint); + } + + if (swapParams.inSubstitute == position.collateralToken) { + _swap(positionId, position.collateralToken, position.debtToken, swapParams.amount, swapParams.data); + } else if (swapParams.inSubstitute == position.debtToken) { + _swap(positionId, position.debtToken, position.collateralToken, swapParams.amount, swapParams.data); + } + + _executeCouponTrade(user, positionId, position.debtToken, couponsToMint, couponsToBurn, interestThreshold); + + if (collateralDelta > 0) { + _mintSubstituteAll(position.collateralToken, user, uint256(collateralDelta)); + IERC20(position.collateralToken).approve(address(_loanPositionManager), uint256(collateralDelta)); + _loanPositionManager.depositToken(position.collateralToken, uint256(collateralDelta)); + } + if (debtDelta < 0) { + _mintSubstituteAll(position.debtToken, user, uint256(-debtDelta)); + IERC20(position.debtToken).approve(address(_loanPositionManager), uint256(-debtDelta)); + _loanPositionManager.depositToken(position.debtToken, uint256(-debtDelta)); + } + if (couponsToBurn.length > 0) { + _unwrapCoupons(couponsToBurn); + _loanPositionManager.burnCoupons(couponsToBurn); + } + + _loanPositionManager.settlePosition(positionId); + } + + function borrow( + address collateralToken, + address debtToken, + uint256 collateralAmount, + uint256 debtAmount, + int256 maxPayInterest, + Epoch expiredWith, + SwapParams calldata swapParams, + ERC20PermitParams calldata collateralPermitParams + ) external payable nonReentrant wrapAndRefundETH returns (uint256 positionId) { + collateralPermitParams.tryPermit(_getUnderlyingToken(collateralToken), msg.sender, address(this)); + + bytes memory lockData = abi.encode(collateralAmount, debtAmount, expiredWith, maxPayInterest); + lockData = abi.encode(0, msg.sender, swapParams, abi.encode(collateralToken, debtToken, lockData)); + bytes memory result = _loanPositionManager.lock(lockData); + positionId = abi.decode(result, (uint256)); + + _burnAllSubstitute(collateralToken, msg.sender); + _burnAllSubstitute(debtToken, msg.sender); + _loanPositionManager.transferFrom(address(this), msg.sender, positionId); + } + + function adjust( + uint256 positionId, + uint256 collateralAmount, + uint256 debtAmount, + int256 interestThreshold, + Epoch expiredWith, + SwapParams calldata swapParams, + PermitSignature calldata positionPermitParams, + ERC20PermitParams calldata collateralPermitParams, + ERC20PermitParams calldata debtPermitParams + ) external payable nonReentrant wrapAndRefundETH onlyPositionOwner(positionId) { + positionPermitParams.tryPermit(_loanPositionManager, positionId, address(this)); + LoanPosition memory position = _loanPositionManager.getPosition(positionId); + collateralPermitParams.tryPermit(_getUnderlyingToken(position.collateralToken), msg.sender, address(this)); + debtPermitParams.tryPermit(_getUnderlyingToken(position.debtToken), msg.sender, address(this)); + + position.collateralAmount = collateralAmount; + position.debtAmount = debtAmount; + position.expiredWith = expiredWith; + + _loanPositionManager.lock(_encodeAdjustData(positionId, position, interestThreshold, swapParams)); + + _burnAllSubstitute(position.collateralToken, msg.sender); + _burnAllSubstitute(position.debtToken, msg.sender); + } + + function _swap( + uint256 positionId, + address inSubstitute, + address outSubstitute, + uint256 inAmount, + bytes memory swapParams + ) internal returns (uint256 outAmount) { + address inToken = ISubstitute(inSubstitute).underlyingToken(); + address outToken = ISubstitute(outSubstitute).underlyingToken(); + uint256 beforeOutTokenBalance = IERC20(outToken).balanceOf(address(this)); + + ISubstitute(inSubstitute).burn(inAmount, address(this)); + if (inToken == address(_weth)) _weth.deposit{value: inAmount}(); + IERC20(inToken).approve(_router, inAmount); + (bool success, bytes memory result) = _router.call(swapParams); + if (!success) revert CollateralSwapFailed(string(result)); + IERC20(inToken).approve(_router, 0); + + unchecked { + outAmount = IERC20(outToken).balanceOf(address(this)) - beforeOutTokenBalance; + } + emit SwapToken(positionId, inToken, outToken, inAmount, outAmount); + + IERC20(outToken).approve(outSubstitute, outAmount); + ISubstitute(outSubstitute).mint(outAmount, address(this)); + } + + function _encodeAdjustData( + uint256 id, + LoanPosition memory p, + int256 interestThreshold, + SwapParams memory swapParams + ) internal view returns (bytes memory) { + bytes memory data = abi.encode(p.collateralAmount, p.debtAmount, p.expiredWith, interestThreshold); + return abi.encode(id, msg.sender, swapParams, data); + } +} diff --git a/contracts/CouponLiquidator.sol b/contracts/CouponLiquidator.sol index d021573..fb8d388 100644 --- a/contracts/CouponLiquidator.sol +++ b/contracts/CouponLiquidator.sol @@ -61,19 +61,15 @@ contract CouponLiquidator is ICouponLiquidator, IPositionLocker { (uint256 liquidationAmount, uint256 repayAmount, uint256 protocolFeeAmount) = _loanPositionManager.liquidate(positionId, maxRepayAmount); - ISubstitute(position.debtToken).ensureBalance(payer, repayAmount); + ISubstitute(position.debtToken).mintAll(payer, repayAmount); IERC20(position.debtToken).approve(address(_loanPositionManager), repayAmount); _loanPositionManager.depositToken(position.debtToken, repayAmount); - uint256 debtAmount = IERC20(outToken).balanceOf(address(this)); - if (debtAmount > 0) { - IERC20(outToken).safeTransfer(recipient, debtAmount); - } - uint256 collateralAmount = liquidationAmount - protocolFeeAmount - swapAmount; _loanPositionManager.withdrawToken(position.collateralToken, address(this), collateralAmount); _burnAllSubstitute(position.collateralToken, recipient); + _burnAllSubstitute(position.debtToken, recipient); return ""; } diff --git a/contracts/DepositControllerV2.sol b/contracts/DepositControllerV2.sol new file mode 100644 index 0000000..9d4bd25 --- /dev/null +++ b/contracts/DepositControllerV2.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: - +// License: https://license.coupon.finance/LICENSE.pdf + +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IDepositControllerV2} from "./interfaces/IDepositControllerV2.sol"; +import {IBondPositionManager} from "./interfaces/IBondPositionManager.sol"; +import {IPositionLocker} from "./interfaces/IPositionLocker.sol"; +import {BondPosition} from "./libraries/BondPosition.sol"; +import {Epoch, EpochLibrary} from "./libraries/Epoch.sol"; +import {CouponKey} from "./libraries/CouponKey.sol"; +import {Coupon} from "./libraries/Coupon.sol"; +import {ISubstitute} from "./interfaces/ISubstitute.sol"; +import {SubstituteLibrary} from "./libraries/Substitute.sol"; +import {ControllerV2} from "./libraries/ControllerV2.sol"; +import {ERC20PermitParams, PermitSignature, PermitParamsLibrary} from "./libraries/PermitParams.sol"; + +contract DepositControllerV2 is IDepositControllerV2, ControllerV2, IPositionLocker { + using PermitParamsLibrary for *; + using EpochLibrary for Epoch; + + IBondPositionManager private immutable _bondPositionManager; + + modifier onlyPositionOwner(uint256 positionId) { + if (_bondPositionManager.ownerOf(positionId) != msg.sender) revert InvalidAccess(); + _; + } + + constructor( + address wrapped1155Factory, + address cloberController, + address bookManager, + address couponManager, + address weth, + address bondPositionManager + ) ControllerV2(wrapped1155Factory, cloberController, bookManager, couponManager, weth) { + _bondPositionManager = IBondPositionManager(bondPositionManager); + } + + function positionLockAcquired(bytes memory data) external returns (bytes memory result) { + if (msg.sender != address(_bondPositionManager)) revert InvalidAccess(); + + uint256 positionId; + address user; + (positionId, user, data) = abi.decode(data, (uint256, address, bytes)); + if (positionId == 0) { + address asset; + (asset, data) = abi.decode(data, (address, bytes)); + positionId = _bondPositionManager.mint(asset); + result = abi.encode(positionId); + } + BondPosition memory position = _bondPositionManager.getPosition(positionId); + + int256 interestThreshold; + (position.amount, position.expiredWith, interestThreshold) = abi.decode(data, (uint256, Epoch, int256)); + (Coupon[] memory couponsToMint, Coupon[] memory couponsToBurn, int256 amountDelta) = + _bondPositionManager.adjustPosition(positionId, position.amount, position.expiredWith); + if (amountDelta < 0) _bondPositionManager.withdrawToken(position.asset, address(this), uint256(-amountDelta)); + if (couponsToMint.length > 0) { + _bondPositionManager.mintCoupons(couponsToMint, address(this), ""); + _wrapCoupons(couponsToMint); + } + + _executeCouponTrade(user, positionId, position.asset, couponsToMint, couponsToBurn, interestThreshold); + + if (amountDelta > 0) { + _mintSubstituteAll(position.asset, user, uint256(amountDelta)); + IERC20(position.asset).approve(address(_bondPositionManager), uint256(amountDelta)); + _bondPositionManager.depositToken(position.asset, uint256(amountDelta)); + } + if (couponsToBurn.length > 0) { + _unwrapCoupons(couponsToBurn); + _bondPositionManager.burnCoupons(couponsToBurn); + } + + _bondPositionManager.settlePosition(positionId); + } + + function deposit( + address asset, + uint256 amount, + Epoch expiredWith, + int256 minEarnInterest, + ERC20PermitParams calldata tokenPermitParams + ) external payable nonReentrant wrapAndRefundETH returns (uint256 positionId) { + tokenPermitParams.tryPermit(_getUnderlyingToken(asset), msg.sender, address(this)); + bytes memory lockData = abi.encode(amount, expiredWith, -minEarnInterest); + bytes memory result = _bondPositionManager.lock(abi.encode(0, msg.sender, abi.encode(asset, lockData))); + positionId = abi.decode(result, (uint256)); + + _burnAllSubstitute(asset, msg.sender); + + _bondPositionManager.transferFrom(address(this), msg.sender, positionId); + } + + function adjust( + uint256 positionId, + uint256 amount, + Epoch expiredWith, + int256 interestThreshold, + ERC20PermitParams calldata tokenPermitParams, + PermitSignature calldata positionPermitParams + ) external payable nonReentrant wrapAndRefundETH onlyPositionOwner(positionId) { + positionPermitParams.tryPermit(_bondPositionManager, positionId, address(this)); + BondPosition memory position = _bondPositionManager.getPosition(positionId); + tokenPermitParams.tryPermit(position.asset, msg.sender, address(this)); + + bytes memory lockData = abi.encode(amount, expiredWith, interestThreshold); + _bondPositionManager.lock(abi.encode(positionId, msg.sender, lockData)); + + _burnAllSubstitute(position.asset, msg.sender); + } +} diff --git a/contracts/SimpleBondController.sol b/contracts/SimpleBondController.sol index bb2fd82..2c2dab3 100644 --- a/contracts/SimpleBondController.sol +++ b/contracts/SimpleBondController.sol @@ -71,7 +71,7 @@ contract SimpleBondController is IPositionLocker, ERC1155Holder, ISimpleBondCont (Coupon[] memory couponsToMint, Coupon[] memory couponsToBurn, int256 amountDelta) = _bondPositionManager.adjustPosition(tokenId, amount, expiredWith); if (amountDelta > 0) { - ISubstitute(asset).ensureBalance(user, uint256(amountDelta)); + ISubstitute(asset).mintAll(user, uint256(amountDelta)); IERC20(asset).approve(address(_bondPositionManager), uint256(amountDelta)); _bondPositionManager.depositToken(address(asset), uint256(amountDelta)); } else if (amountDelta < 0) { diff --git a/contracts/external/clober-v2/BookId.sol b/contracts/external/clober-v2/BookId.sol new file mode 100644 index 0000000..fbfb48b --- /dev/null +++ b/contracts/external/clober-v2/BookId.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.20; + +import {IBookManager} from "./IBookManager.sol"; + +type BookId is uint192; + +library BookIdLibrary { + function toId(IBookManager.BookKey memory bookKey) internal pure returns (BookId id) { + bytes32 hash = keccak256(abi.encode(bookKey)); + assembly { + id := hash + } + } +} diff --git a/contracts/external/clober-v2/Currency.sol b/contracts/external/clober-v2/Currency.sol new file mode 100644 index 0000000..a6d17b2 --- /dev/null +++ b/contracts/external/clober-v2/Currency.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +type Currency is address; + +/// @title CurrencyLibrary +/// @dev This library allows for transferring and holding native tokens and ERC20 tokens +library CurrencyLibrary { + using CurrencyLibrary for Currency; + + /// @notice Thrown when a native transfer fails + error NativeTransferFailed(); + + /// @notice Thrown when an ERC20 transfer fails + error ERC20TransferFailed(); + + Currency public constant NATIVE = Currency.wrap(address(0)); + + function transfer(Currency currency, address to, uint256 amount) internal { + // implementation from + // https://github.com/transmissions11/solmate/blob/e8f96f25d48fe702117ce76c79228ca4f20206cb/src/utils/SafeTransferLib.sol + + bool success; + if (currency.isNative()) { + assembly { + // Transfer the ETH and store if it succeeded or not. + success := call(gas(), to, amount, 0, 0, 0, 0) + } + + if (!success) revert NativeTransferFailed(); + } else { + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata into memory, beginning with the function selector. + mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. + + success := + and( + // Set success to whether the call reverted, if not we check it either + // returned exactly 1 (can't just be non-zero data), or had no return data. + or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), + // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. + // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. + // Counterintuitively, this call must be positioned second to the or() call in the + // surrounding and() call or else returndatasize() will be zero during the computation. + call(gas(), currency, 0, freeMemoryPointer, 68, 0, 32) + ) + } + + if (!success) revert ERC20TransferFailed(); + } + } + + function balanceOfSelf(Currency currency) internal view returns (uint256) { + if (currency.isNative()) return address(this).balance; + else return IERC20(Currency.unwrap(currency)).balanceOf(address(this)); + } + + function equals(Currency currency, Currency other) internal pure returns (bool) { + return Currency.unwrap(currency) == Currency.unwrap(other); + } + + function isNative(Currency currency) internal pure returns (bool) { + return Currency.unwrap(currency) == Currency.unwrap(NATIVE); + } + + function toId(Currency currency) internal pure returns (uint256) { + return uint160(Currency.unwrap(currency)); + } + + function fromId(uint256 id) internal pure returns (Currency) { + return Currency.wrap(address(uint160(id))); + } +} diff --git a/contracts/external/clober-v2/FeePolicy.sol b/contracts/external/clober-v2/FeePolicy.sol new file mode 100644 index 0000000..38f0e09 --- /dev/null +++ b/contracts/external/clober-v2/FeePolicy.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.20; + +import {Math} from "./Math.sol"; + +type FeePolicy is uint24; + +library FeePolicyLibrary { + uint256 internal constant RATE_PRECISION = 10 ** 6; + int256 internal constant MAX_FEE_RATE = 500000; + int256 internal constant MIN_FEE_RATE = -500000; + + uint256 internal constant RATE_MASK = 0x7fffff; // 23 bits + + error InvalidFeePolicy(); + + function encode(bool usesQuote_, int24 rate_) internal pure returns (FeePolicy feePolicy) { + if (rate_ > MAX_FEE_RATE || rate_ < MIN_FEE_RATE) { + revert InvalidFeePolicy(); + } + + uint256 mask = usesQuote_ ? 1 << 23 : 0; + assembly { + feePolicy := or(mask, add(rate_, MAX_FEE_RATE)) + } + } + + function isValid(FeePolicy self) internal pure returns (bool) { + int24 r = rate(self); + + return !(r > MAX_FEE_RATE || r < MIN_FEE_RATE); + } + + function usesQuote(FeePolicy self) internal pure returns (bool f) { + assembly { + f := shr(23, self) + } + } + + function rate(FeePolicy self) internal pure returns (int24 r) { + assembly { + r := sub(and(self, RATE_MASK), MAX_FEE_RATE) + } + } + + function calculateFee(FeePolicy self, uint256 amount, bool reverseRounding) internal pure returns (int256 fee) { + int24 r = rate(self); + + bool positive = r > 0; + uint256 absRate; + unchecked { + absRate = uint256(uint24(positive ? r : -r)); + } + // @dev absFee must be less than type(int256).max + uint256 absFee = Math.divide(amount * absRate, RATE_PRECISION, reverseRounding ? !positive : positive); + fee = positive ? int256(absFee) : -int256(absFee); + } + + function calculateOriginalAmount(FeePolicy self, uint256 amount, bool reverseFee) + internal + pure + returns (uint256 originalAmount) + { + int24 r = rate(self); + + bool positive = r > 0; + uint256 divider; + assembly { + if reverseFee { r := sub(0, r) } + divider := add(RATE_PRECISION, r) + } + originalAmount = Math.divide(amount * RATE_PRECISION, divider, positive); + } +} diff --git a/contracts/external/clober-v2/IBookManager.sol b/contracts/external/clober-v2/IBookManager.sol new file mode 100644 index 0000000..d0658de --- /dev/null +++ b/contracts/external/clober-v2/IBookManager.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; + +import {BookId} from "./BookId.sol"; +import {Currency} from "./Currency.sol"; +import {OrderId} from "./OrderId.sol"; +import {Tick} from "./Tick.sol"; +import {FeePolicy} from "./FeePolicy.sol"; +import {IERC721Permit} from "./IERC721Permit.sol"; +import {IHooks} from "./IHooks.sol"; + +/** + * @title IBookManager + * @notice The interface for the BookManager contract + */ +interface IBookManager is IERC721Metadata, IERC721Permit { + error InvalidUnit(); + error InvalidFeePolicy(); + error InvalidProvider(address provider); + error LockedBy(address locker, address hook); + error CurrencyNotSettled(); + + /** + * @notice Event emitted when a new book is opened + * @param id The book id + * @param base The base currency + * @param quote The quote currency + * @param unit The unit of the book + * @param makerPolicy The maker fee policy + * @param takerPolicy The taker fee policy + * @param hooks The hooks contract + */ + event Open( + BookId indexed id, + Currency indexed base, + Currency indexed quote, + uint64 unit, + FeePolicy makerPolicy, + FeePolicy takerPolicy, + IHooks hooks + ); + + /** + * @notice Event emitted when a new order is made + * @param bookId The book id + * @param user The user address + * @param tick The order tick + * @param orderIndex The order index + * @param amount The order amount + * @param provider The provider address + */ + event Make( + BookId indexed bookId, address indexed user, Tick tick, uint256 orderIndex, uint64 amount, address provider + ); + + /** + * @notice Event emitted when an order is taken + * @param bookId The book id + * @param user The user address + * @param tick The order tick + * @param amount The order amount + */ + event Take(BookId indexed bookId, address indexed user, Tick tick, uint64 amount); + + /** + * @notice Event emitted when an order is canceled + * @param orderId The order id + * @param canceledAmount The canceled amount + */ + event Cancel(OrderId indexed orderId, uint64 canceledAmount); + + /** + * @notice Event emitted when an order is claimed + * @param orderId The order id + * @param rawAmount The claimed amount + */ + event Claim(OrderId indexed orderId, uint64 rawAmount); + + /** + * @notice Event emitted when a provider is whitelisted + * @param provider The provider address + */ + event Whitelist(address indexed provider); + + /** + * @notice Event emitted when a provider is delisted + * @param provider The provider address + */ + event Delist(address indexed provider); + + /** + * @notice Event emitted when a provider collects fees + * @param provider The provider address + * @param currency The currency + * @param amount The collected amount + */ + event Collect(address indexed provider, Currency indexed currency, uint256 amount); + + /** + * @notice Event emitted when new default provider is set + * @param newDefaultProvider The new default provider address + */ + event SetDefaultProvider(address indexed newDefaultProvider); + + struct BookKey { + Currency base; + uint64 unit; + Currency quote; + FeePolicy makerPolicy; + IHooks hooks; + FeePolicy takerPolicy; + } + + /** + * @notice Returns the base URI + * @return The base URI + */ + function baseURI() external view returns (string memory); + + /** + * @notice Returns the contract URI + * @return The contract URI + */ + function contractURI() external view returns (string memory); + + /** + * @notice Returns the default provider + * @return The default provider + */ + function defaultProvider() external view returns (address); + + /** + * @notice Calculates the currency balance changes for a given locker + * @param locker The address of the locker + * @param currency The currency in question + * @return The net change in currency balance + */ + function currencyDelta(address locker, Currency currency) external view returns (int256); + + /** + * @notice Returns the total reserves of a given currency + * @param currency The currency in question + * @return The total reserves amount + */ + function reservesOf(Currency currency) external view returns (uint256); + + /** + * @notice Checks if a provider is whitelisted + * @param provider The address of the provider + * @return True if the provider is whitelisted, false otherwise + */ + function isWhitelisted(address provider) external view returns (bool); + + /** + * @notice Verifies if an owner has authorized a spender for a token + * @param owner The address of the token owner + * @param spender The address of the spender + * @param tokenId The token ID + */ + function checkAuthorized(address owner, address spender, uint256 tokenId) external view; + + /** + * @notice Calculates the amount owed to a provider in a given currency + * @param provider The provider's address + * @param currency The currency in question + * @return The owed amount + */ + function tokenOwed(address provider, Currency currency) external view returns (uint256); + + /** + * @notice Retrieves the book key for a given book ID + * @param id The book ID + * @return The book key + */ + function getBookKey(BookId id) external view returns (BookKey memory); + + struct OrderInfo { + address provider; + uint64 open; + uint64 claimable; + } + + /** + * @notice Provides information about an order + * @param id The order ID + * @return Order information including provider, open status, and claimable amount + */ + function getOrder(OrderId id) external view returns (OrderInfo memory); + + /** + * @notice Retrieves the locker and caller addresses for a given lock + * @param i The index of the lock + * @return locker The locker's address + * @return lockCaller The caller's address + */ + function getLock(uint256 i) external view returns (address locker, address lockCaller); + + /** + * @notice Provides the lock data + * @return The lock data including necessary numeric values + */ + function getLockData() external view returns (uint128, uint128); + + /** + * @notice Returns the depth of a given book ID and tick + * @param id The book ID + * @param tick The tick + * @return The depth of the tick + */ + function getDepth(BookId id, Tick tick) external view returns (uint64); + + /** + * @notice Retrieves the highest tick for a given book ID + * @param id The book ID + * @return tick The highest tick + */ + function getHighest(BookId id) external view returns (Tick tick); + + /** + * @notice Finds the maximum tick less than a specified tick in a book + * @dev Returns `Tick.wrap(type(int24).min)` if the specified tick is the lowest + * @param id The book ID + * @param tick The specified tick + * @return The next lower tick + */ + function maxLessThan(BookId id, Tick tick) external view returns (Tick); + + /** + * @notice Checks if a book is empty + * @param id The book ID + * @return True if the book is empty, false otherwise + */ + function isEmpty(BookId id) external view returns (bool); + + /** + * @notice Loads a value from a specific storage slot + * @param slot The storage slot + * @return The value in the slot + */ + function load(bytes32 slot) external view returns (bytes32); + + /** + * @notice Loads a sequence of values starting from a specific slot + * @param startSlot The starting slot + * @param nSlot The number of slots to load + * @return The sequence of values + */ + function load(bytes32 startSlot, uint256 nSlot) external view returns (bytes memory); + + /** + * @notice Opens a new book + * @param key The book key + * @param hookData The hook data + */ + function open(BookKey calldata key, bytes calldata hookData) external; + + /** + * @notice Locks a book manager function + * @param locker The locker address + * @param data The lock data + * @return The lock return data + */ + function lock(address locker, bytes calldata data) external returns (bytes memory); + + struct MakeParams { + BookKey key; + Tick tick; + uint64 amount; // times 10**unitDecimals to get actual bid amount + /// @notice The limit order service provider address to collect fees + address provider; + } + + /** + * @notice Make a limit order + * @param params The order parameters + * @param hookData The hook data + * @return id The order id. Returns 0 if the order is not settled + * @return quoteAmount The amount of quote currency to be paid + */ + function make(MakeParams calldata params, bytes calldata hookData) + external + returns (OrderId id, uint256 quoteAmount); + + struct TakeParams { + BookKey key; + Tick tick; + uint64 maxAmount; + } + + /** + * @notice Take a limit order at specific tick + * @param params The order parameters + * @param hookData The hook data + * @return quoteAmount The amount of quote currency to be received + * @return baseAmount The amount of base currency to be paid + */ + function take(TakeParams calldata params, bytes calldata hookData) + external + returns (uint256 quoteAmount, uint256 baseAmount); + + struct CancelParams { + OrderId id; + uint64 to; + } + + /** + * @notice Cancel a limit order + * @param params The order parameters + * @param hookData The hook data + * @return canceledAmount The amount of quote currency canceled + */ + function cancel(CancelParams calldata params, bytes calldata hookData) external returns (uint256 canceledAmount); + + /** + * @notice Claims an order + * @param id The order ID + * @param hookData The hook data + * @return claimedAmount The amount claimed + */ + function claim(OrderId id, bytes calldata hookData) external returns (uint256 claimedAmount); + + /** + * @notice Collects fees from a provider + * @param provider The provider address + * @param currency The currency + */ + function collect(address provider, Currency currency) external; + + /** + * @notice Withdraws a currency + * @param currency The currency + * @param to The recipient address + * @param amount The amount + */ + function withdraw(Currency currency, address to, uint256 amount) external; + + /** + * @notice Settles a currency + * @param currency The currency + * @return The settled amount + */ + function settle(Currency currency) external payable returns (uint256); + + /** + * @notice Whitelists a provider + * @param provider The provider address + */ + function whitelist(address provider) external; + + /** + * @notice Delists a provider + * @param provider The provider address + */ + function delist(address provider) external; + + /** + * @notice Sets the default provider + * @param newDefaultProvider The new default provider address + */ + function setDefaultProvider(address newDefaultProvider) external; +} diff --git a/contracts/external/clober-v2/IController.sol b/contracts/external/clober-v2/IController.sol new file mode 100644 index 0000000..c0f84be --- /dev/null +++ b/contracts/external/clober-v2/IController.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; + +import {OrderId} from "./OrderId.sol"; +import {BookId} from "./BookId.sol"; +import {Tick} from "./Tick.sol"; +import {IBookManager} from "./IBookManager.sol"; + +/** + * @title IController + * @notice Interface for the controller contract + */ +interface IController { + // Error messages + error InvalidAccess(); + error InvalidLength(); + error Deadline(); + error InvalidMarket(); + error ControllerSlippage(); + error ValueTransferFailed(); + error InvalidAction(); + + /** + * @notice Enum for the different actions that can be performed + */ + enum Action { + OPEN, + MAKE, + LIMIT, + TAKE, + SPEND, + CLAIM, + CANCEL + } + + /** + * @notice Struct for the parameters of the ERC20 permit + */ + struct ERC20PermitParams { + address token; + uint256 permitAmount; + PermitSignature signature; + } + + /** + * @notice Struct for the parameters of the ERC721 permit + */ + struct ERC721PermitParams { + uint256 tokenId; + PermitSignature signature; + } + + /** + * @notice Struct for the signature of the permit + */ + struct PermitSignature { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + /** + * @notice Struct for the parameters of the open book action + */ + struct OpenBookParams { + IBookManager.BookKey key; + bytes hookData; + } + + /** + * @notice Struct for the parameters of the make order action + */ + struct MakeOrderParams { + BookId id; + Tick tick; + uint256 quoteAmount; + bytes hookData; + } + + /** + * @notice Struct for the parameters of the limit order action + */ + struct LimitOrderParams { + BookId takeBookId; + BookId makeBookId; + uint256 limitPrice; + Tick tick; + uint256 quoteAmount; + bytes takeHookData; + bytes makeHookData; + } + + /** + * @notice Struct for the parameters of the take order action + */ + struct TakeOrderParams { + BookId id; + uint256 limitPrice; + uint256 quoteAmount; + bytes hookData; + } + + /** + * @notice Struct for the parameters of the spend order action + */ + struct SpendOrderParams { + BookId id; + uint256 limitPrice; + uint256 baseAmount; + bytes hookData; + } + + /** + * @notice Struct for the parameters of the claim order action + */ + struct ClaimOrderParams { + OrderId id; + bytes hookData; + } + + /** + * @notice Struct for the parameters of the cancel order action + */ + struct CancelOrderParams { + OrderId id; + uint256 leftQuoteAmount; + bytes hookData; + } + + /** + * @notice Opens a book + * @param openBookParamsList The parameters of the open book action + * @param deadline The deadline for the action + */ + function open(OpenBookParams[] calldata openBookParamsList, uint64 deadline) external; + + /** + * @notice Returns the depth of a book + * @param id The id of the book + * @param tick The tick of the book + * @return The depth of the book in quote amount + */ + function getDepth(BookId id, Tick tick) external view returns (uint256); + + /** + * @notice Returns the highest price of a book + * @param id The id of the book + * @return The highest price of the book with 2**128 precision + */ + function getHighestPrice(BookId id) external view returns (uint256); + + /** + * @notice Returns the details of an order + * @param orderId The id of the order + * @return provider The provider of the order + * @return price The price of the order with 2**128 precision + * @return openAmount The open quote amount of the order + * @return claimableAmount The claimable base amount of the order + */ + function getOrder(OrderId orderId) + external + view + returns (address provider, uint256 price, uint256 openAmount, uint256 claimableAmount); + + /** + * @notice Converts a price to a tick + * @param price The price to convert + * @return The tick + */ + function fromPrice(uint256 price) external pure returns (Tick); + + /** + * @notice Converts a tick to a price + * @param tick The tick to convert + * @return The price with 2**128 precision + */ + function toPrice(Tick tick) external pure returns (uint256); + + /** + * @notice Executes a list of actions + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param actionList The list of actions to execute + * @param paramsDataList The parameters of the actions + * @param tokensToSettle The tokens to settle + * @param erc20PermitParamsList The parameters of the ERC20 permits + * @param erc721PermitParamsList The parameters of the ERC721 permits + * @param deadline The deadline for the actions + * @return ids The ids of the orders + */ + function execute( + Action[] calldata actionList, + bytes[] calldata paramsDataList, + address[] calldata tokensToSettle, + ERC20PermitParams[] calldata erc20PermitParamsList, + ERC721PermitParams[] calldata erc721PermitParamsList, + uint64 deadline + ) external payable returns (OrderId[] memory ids); + + /** + * @notice Makes a list of orders + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param orderParamsList The list of actions to make + * @param tokensToSettle The tokens to settle + * @param permitParamsList The parameters of the permits + * @param deadline The deadline for the actions + * @return ids The ids of the orders + */ + function make( + MakeOrderParams[] calldata orderParamsList, + address[] calldata tokensToSettle, + ERC20PermitParams[] calldata permitParamsList, + uint64 deadline + ) external payable returns (OrderId[] memory ids); + + /** + * @notice Takes a list of orders + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param orderParamsList The list of actions to take + * @param tokensToSettle The tokens to settle + * @param permitParamsList The parameters of the permits + * @param deadline The deadline for the actions + */ + function take( + TakeOrderParams[] calldata orderParamsList, + address[] calldata tokensToSettle, + ERC20PermitParams[] calldata permitParamsList, + uint64 deadline + ) external payable; + + /** + * @notice Spends to take a list of orders + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param orderParamsList The list of actions to spend + * @param tokensToSettle The tokens to settle + * @param permitParamsList The parameters of the permits + * @param deadline The deadline for the actions + */ + function spend( + SpendOrderParams[] calldata orderParamsList, + address[] calldata tokensToSettle, + ERC20PermitParams[] calldata permitParamsList, + uint64 deadline + ) external payable; + + /** + * @notice Claims a list of orders + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param orderParamsList The list of actions to claim + * @param tokensToSettle The tokens to settle + * @param permitParamsList The parameters of the permits + * @param deadline The deadline for the actions + */ + function claim( + ClaimOrderParams[] calldata orderParamsList, + address[] calldata tokensToSettle, + ERC721PermitParams[] calldata permitParamsList, + uint64 deadline + ) external; + + /** + * @notice Cancels a list of orders + * @dev IMPORTANT: The caller must provide `tokensToSettle` to receive appropriate tokens after execution. + * @param orderParamsList The list of actions to cancel + * @param tokensToSettle The tokens to settle + * @param permitParamsList The parameters of the permits + * @param deadline The deadline for the actions + */ + function cancel( + CancelOrderParams[] calldata orderParamsList, + address[] calldata tokensToSettle, + ERC721PermitParams[] calldata permitParamsList, + uint64 deadline + ) external; +} diff --git a/contracts/external/clober-v2/IERC721Permit.sol b/contracts/external/clober-v2/IERC721Permit.sol new file mode 100644 index 0000000..cf2156c --- /dev/null +++ b/contracts/external/clober-v2/IERC721Permit.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title IERC721Permit + * @notice An interface for the ERC721 permit extension + */ +interface IERC721Permit is IERC721 { + error InvalidSignature(); + error PermitExpired(); + + /** + * @notice The EIP-712 typehash for the permit struct used by the contract + */ + function PERMIT_TYPEHASH() external pure returns (bytes32); + + /** + * @notice The EIP-712 domain separator for this contract + */ + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /** + * @notice Approve the spender to transfer the given tokenId + * @param spender The address to approve + * @param tokenId The tokenId to approve + * @param deadline The deadline for the signature + * @param v The recovery id of the signature + * @param r The r value of the signature + * @param s The s value of the signature + */ + function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + + /** + * @notice Get the current nonce for a token + * @param tokenId The tokenId to get the nonce for + * @return The current nonce + */ + function nonces(uint256 tokenId) external view returns (uint256); +} diff --git a/contracts/external/clober-v2/IHooks.sol b/contracts/external/clober-v2/IHooks.sol new file mode 100644 index 0000000..79cff17 --- /dev/null +++ b/contracts/external/clober-v2/IHooks.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.20; + +import {IBookManager} from "./IBookManager.sol"; +import {OrderId} from "./OrderId.sol"; + +/** + * @title IHooks + * @notice Interface for the hooks contract + */ +interface IHooks { + /** + * @notice Hook called before opening a new book + * @param sender The sender of the open transaction + * @param key The key of the book being opened + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function beforeOpen(address sender, IBookManager.BookKey calldata key, bytes calldata hookData) + external + returns (bytes4); + + /** + * @notice Hook called after opening a new book + * @param sender The sender of the open transaction + * @param key The key of the book being opened + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function afterOpen(address sender, IBookManager.BookKey calldata key, bytes calldata hookData) + external + returns (bytes4); + + /** + * @notice Hook called before making a new order + * @param sender The sender of the make transaction + * @param params The parameters of the make transaction + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function beforeMake(address sender, IBookManager.MakeParams calldata params, bytes calldata hookData) + external + returns (bytes4); + + /** + * @notice Hook called after making a new order + * @param sender The sender of the make transaction + * @param params The parameters of the make transaction + * @param orderId The id of the order that was made + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function afterMake( + address sender, + IBookManager.MakeParams calldata params, + OrderId orderId, + bytes calldata hookData + ) external returns (bytes4); + + /** + * @notice Hook called before taking an order + * @param sender The sender of the take transaction + * @param params The parameters of the take transaction + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function beforeTake(address sender, IBookManager.TakeParams calldata params, bytes calldata hookData) + external + returns (bytes4); + + /** + * @notice Hook called after taking an order + * @param sender The sender of the take transaction + * @param params The parameters of the take transaction + * @param takenAmount The amount that was taken + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function afterTake( + address sender, + IBookManager.TakeParams calldata params, + uint64 takenAmount, + bytes calldata hookData + ) external returns (bytes4); + + /** + * @notice Hook called before canceling an order + * @param sender The sender of the cancel transaction + * @param params The parameters of the cancel transaction + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function beforeCancel(address sender, IBookManager.CancelParams calldata params, bytes calldata hookData) + external + returns (bytes4); + + /** + * @notice Hook called after canceling an order + * @param sender The sender of the cancel transaction + * @param params The parameters of the cancel transaction + * @param canceledAmount The amount that was canceled + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function afterCancel( + address sender, + IBookManager.CancelParams calldata params, + uint64 canceledAmount, + bytes calldata hookData + ) external returns (bytes4); + + /** + * @notice Hook called before claiming an order + * @param sender The sender of the claim transaction + * @param orderId The id of the order being claimed + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function beforeClaim(address sender, OrderId orderId, bytes calldata hookData) external returns (bytes4); + + /** + * @notice Hook called after claiming an order + * @param sender The sender of the claim transaction + * @param orderId The id of the order being claimed + * @param claimedAmount The amount that was claimed + * @param hookData The data passed to the hook + * @return Returns the function selector if the hook is successful + */ + function afterClaim(address sender, OrderId orderId, uint64 claimedAmount, bytes calldata hookData) + external + returns (bytes4); +} diff --git a/contracts/external/clober-v2/Math.sol b/contracts/external/clober-v2/Math.sol new file mode 100644 index 0000000..37b524c --- /dev/null +++ b/contracts/external/clober-v2/Math.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {SignificantBit} from "./SignificantBit.sol"; + +library Math { + using SignificantBit for uint256; + + function divide(uint256 a, uint256 b, bool roundingUp) internal pure returns (uint256 ret) { + // In the OrderBook contract code, b is never zero. + assembly { + ret := add(div(a, b), and(gt(mod(a, b), 0), roundingUp)) + } + } + + function log2(uint256 x) internal pure returns (int256) { + require(x > 0); + + uint8 msb = x.mostSignificantBit(); + + if (msb > 128) x >>= msb - 128; + else if (msb < 128) x <<= 128 - msb; + + x &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + int256 result = (int256(uint256(msb)) - 128) << 128; // Integer part of log_2 + + int256 bit = 0x80000000000000000000000000000000; + for (uint8 i = 0; i < 128 && x > 0; i++) { + x = (x << 1) + ((x * x + 0x80000000000000000000000000000000) >> 128); + if (x > 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) { + result |= bit; + x = (x >> 1) - 0x80000000000000000000000000000000; + } + bit >>= 1; + } + + return result; + } +} diff --git a/contracts/external/clober-v2/OrderId.sol b/contracts/external/clober-v2/OrderId.sol new file mode 100644 index 0000000..49c3284 --- /dev/null +++ b/contracts/external/clober-v2/OrderId.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {Tick} from "./Tick.sol"; +import {BookId} from "./BookId.sol"; + +type OrderId is uint256; + +library OrderIdLibrary { + /** + * @dev Encode the order id. + * @param bookId The book id. + * @param tick The tick. + * @param index The index. + * @return id The order id. + */ + function encode(BookId bookId, Tick tick, uint40 index) internal pure returns (OrderId id) { + // @dev If we just use tick at the assembly code, the code will convert tick into bytes32. + // e.g. When index == -2, the shifted value( shl(40, tick) ) will be + // 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0000000000 instead of 0xfffffffe0000000000 + // Therefore, we have to safely cast tick into uint256 first. + uint256 _tick = uint256(uint24(Tick.unwrap(tick))); + assembly { + id := add(index, add(shl(40, _tick), shl(64, bookId))) + } + } + + function decode(OrderId id) internal pure returns (BookId bookId, Tick tick, uint40 index) { + assembly { + bookId := shr(64, id) + tick := shr(40, id) + index := id + } + } + + function getBookId(OrderId id) internal pure returns (BookId bookId) { + assembly { + bookId := shr(64, id) + } + } +} diff --git a/contracts/external/clober-v2/SignificantBit.sol b/contracts/external/clober-v2/SignificantBit.sol new file mode 100644 index 0000000..46517ed --- /dev/null +++ b/contracts/external/clober-v2/SignificantBit.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +library SignificantBit { + // http://supertech.csail.mit.edu/papers/debruijn.pdf + uint256 internal constant DEBRUIJN_SEQ = 0x818283848586878898A8B8C8D8E8F929395969799A9B9D9E9FAAEB6BEDEEFF; + bytes internal constant DEBRUIJN_INDEX = + hex"0001020903110a19042112290b311a3905412245134d2a550c5d32651b6d3a7506264262237d468514804e8d2b95569d0d495ea533a966b11c886eb93bc176c9071727374353637324837e9b47af86c7155181ad4fd18ed32c9096db57d59ee30e2e4a6a5f92a6be3498aae067ddb2eb1d5989b56fd7baf33ca0c2ee77e5caf7ff0810182028303840444c545c646c7425617c847f8c949c48a4a8b087b8c0c816365272829aaec650acd0d28fdad4e22d6991bd97dfdcea58b4d6f29fede4f6fe0f1f2f3f4b5b6b607b8b93a3a7b7bf357199c5abcfd9e168bcdee9b3f1ecf5fd1e3e5a7a8aa2b670c4ced8bbe8f0f4fc3d79a1c3cde7effb78cce6facbf9f8"; + + /** + * @notice Finds the index of the least significant bit. + * @param x The value to compute the least significant bit for. Must be a non-zero value. + * @return ret The index of the least significant bit. + */ + function leastSignificantBit(uint256 x) internal pure returns (uint8) { + require(x > 0); + uint256 index; + assembly { + index := shr(248, mul(and(x, add(not(x), 1)), DEBRUIJN_SEQ)) + } + return uint8(DEBRUIJN_INDEX[index]); // can optimize with CODECOPY opcode + } + + function mostSignificantBit(uint256 x) internal pure returns (uint8) { + require(x > 0); + uint256 msb; + assembly { + let f := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(5, gt(x, 0xFFFFFFFF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(4, gt(x, 0xFFFF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(3, gt(x, 0xFF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(2, gt(x, 0xF)) + msb := or(msb, f) + x := shr(f, x) + f := shl(1, gt(x, 0x3)) + msb := or(msb, f) + x := shr(f, x) + f := gt(x, 0x1) + msb := or(msb, f) + } + return uint8(msb); + } +} diff --git a/contracts/external/clober-v2/Tick.sol b/contracts/external/clober-v2/Tick.sol new file mode 100644 index 0000000..f3f57eb --- /dev/null +++ b/contracts/external/clober-v2/Tick.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.20; + +import {Math} from "./Math.sol"; + +type Tick is int24; + +library TickLibrary { + using Math for uint256; + using TickLibrary for Tick; + + error InvalidTick(); + error InvalidPrice(); + error TickOverflow(); + + int24 internal constant MAX_TICK = 2 ** 19 - 1; + int24 internal constant MIN_TICK = -MAX_TICK; + + uint256 internal constant MIN_PRICE = 5800731190957938; + uint256 internal constant MAX_PRICE = 19961636804996334433808922353085948875386438476189866322430503; + + uint256 private constant _R0 = 0xfff97272373d413259a46990580e2139; // 2^128 / r^(2^0) + uint256 private constant _R1 = 0xfff2e50f5f656932ef12357cf3c7fdcb; + uint256 private constant _R2 = 0xffe5caca7e10e4e61c3624eaa0941ccf; + uint256 private constant _R3 = 0xffcb9843d60f6159c9db58835c926643; + uint256 private constant _R4 = 0xff973b41fa98c081472e6896dfb254bf; + uint256 private constant _R5 = 0xff2ea16466c96a3843ec78b326b52860; + uint256 private constant _R6 = 0xfe5dee046a99a2a811c461f1969c3052; + uint256 private constant _R7 = 0xfcbe86c7900a88aedcffc83b479aa3a3; + uint256 private constant _R8 = 0xf987a7253ac413176f2b074cf7815e53; + uint256 private constant _R9 = 0xf3392b0822b70005940c7a398e4b70f2; + uint256 private constant _R10 = 0xe7159475a2c29b7443b29c7fa6e889d8; + uint256 private constant _R11 = 0xd097f3bdfd2022b8845ad8f792aa5825; + uint256 private constant _R12 = 0xa9f746462d870fdf8a65dc1f90e061e4; + uint256 private constant _R13 = 0x70d869a156d2a1b890bb3df62baf32f6; + uint256 private constant _R14 = 0x31be135f97d08fd981231505542fcfa5; + uint256 private constant _R15 = 0x9aa508b5b7a84e1c677de54f3e99bc8; + uint256 private constant _R16 = 0x5d6af8dedb81196699c329225ee604; + uint256 private constant _R17 = 0x2216e584f5fa1ea926041bedfe97; + uint256 private constant _R18 = 0x48a170391f7dc42444e8fa2; + + function validateTick(Tick tick) internal pure { + if (Tick.unwrap(tick) > MAX_TICK || Tick.unwrap(tick) < MIN_TICK) revert InvalidTick(); + } + + modifier validatePrice(uint256 price) { + if (price > MAX_PRICE || price < MIN_PRICE) revert InvalidPrice(); + _; + } + + function toTick(uint24 x) internal pure returns (Tick t) { + assembly { + t := sub(x, 0x800000) + } + } + + function toUint24(Tick tick) internal pure returns (uint24 r) { + assembly { + r := add(tick, 0x800000) + } + } + + function fromPrice(uint256 price) internal pure validatePrice(price) returns (Tick) { + int256 log = price.log2(); + int256 tick = log / 49089913871092318234424474366155889; + int256 tickLow = ( + log - int256(uint256((price >> 128 == 0) ? 49089913871092318234424474366155887 : 84124744249948177485425)) + ) / 49089913871092318234424474366155889; + + if (tick == tickLow) return Tick.wrap(int24(tick)); + + if (toPrice(Tick.wrap(int24(tick))) <= price) return Tick.wrap(int24(tick)); + + return Tick.wrap(int24(tickLow)); + } + + function toPrice(Tick tick) internal pure returns (uint256 price) { + validateTick(tick); + int24 tickValue = Tick.unwrap(tick); + uint256 absTick = uint24(tickValue < 0 ? -tickValue : tickValue); + + unchecked { + if (absTick & 0x1 != 0) price = _R0; + else price = 1 << 128; + if (absTick & 0x2 != 0) price = (price * _R1) >> 128; + if (absTick & 0x4 != 0) price = (price * _R2) >> 128; + if (absTick & 0x8 != 0) price = (price * _R3) >> 128; + if (absTick & 0x10 != 0) price = (price * _R4) >> 128; + if (absTick & 0x20 != 0) price = (price * _R5) >> 128; + if (absTick & 0x40 != 0) price = (price * _R6) >> 128; + if (absTick & 0x80 != 0) price = (price * _R7) >> 128; + if (absTick & 0x100 != 0) price = (price * _R8) >> 128; + if (absTick & 0x200 != 0) price = (price * _R9) >> 128; + if (absTick & 0x400 != 0) price = (price * _R10) >> 128; + if (absTick & 0x800 != 0) price = (price * _R11) >> 128; + if (absTick & 0x1000 != 0) price = (price * _R12) >> 128; + if (absTick & 0x2000 != 0) price = (price * _R13) >> 128; + if (absTick & 0x4000 != 0) price = (price * _R14) >> 128; + if (absTick & 0x8000 != 0) price = (price * _R15) >> 128; + if (absTick & 0x10000 != 0) price = (price * _R16) >> 128; + if (absTick & 0x20000 != 0) price = (price * _R17) >> 128; + if (absTick & 0x40000 != 0) price = (price * _R18) >> 128; + } + if (tickValue > 0) price = type(uint256).max / price; + } + + function gt(Tick a, Tick b) internal pure returns (bool) { + return Tick.unwrap(a) > Tick.unwrap(b); + } + + function baseToQuote(Tick tick, uint256 base, bool roundingUp) internal pure returns (uint256) { + return Math.divide((base * tick.toPrice()), 1 << 128, roundingUp); + } + + function quoteToBase(Tick tick, uint256 quote, bool roundingUp) internal pure returns (uint256) { + // @dev quote = raw(uint64) * unit(uint64) < 2^128 + // We don't need to check overflow here + return Math.divide(quote << 128, tick.toPrice(), roundingUp); + } +} diff --git a/contracts/interfaces/IBorrowControllerV2.sol b/contracts/interfaces/IBorrowControllerV2.sol new file mode 100644 index 0000000..815bc7c --- /dev/null +++ b/contracts/interfaces/IBorrowControllerV2.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IControllerV2} from "./IControllerV2.sol"; +import {ERC20PermitParams, PermitSignature} from "../libraries/PermitParams.sol"; +import {Epoch} from "../libraries/Epoch.sol"; + +interface IBorrowControllerV2 is IControllerV2 { + event SwapToken( + uint256 indexed positionId, + address indexed inToken, + address indexed outToken, + uint256 inAmount, + uint256 outAmount + ); + + struct SwapParams { + address inSubstitute; + uint256 amount; + bytes data; + } + + error CollateralSwapFailed(string reason); + + function borrow( + address collateralToken, + address debtToken, + uint256 collateralAmount, + uint256 debtAmount, + int256 maxPayInterest, + Epoch expiredWith, + SwapParams calldata swapParams, + ERC20PermitParams calldata collateralPermitParams + ) external payable returns (uint256 positionId); + + function adjust( + uint256 positionId, + uint256 collateralAmount, + uint256 debtAmount, + int256 interestThreshold, + Epoch expiredWith, + SwapParams calldata swapParams, + PermitSignature calldata positionPermitParams, + ERC20PermitParams calldata collateralPermitParams, + ERC20PermitParams calldata debtPermitParams + ) external payable; +} diff --git a/contracts/interfaces/IControllerV2.sol b/contracts/interfaces/IControllerV2.sol new file mode 100644 index 0000000..a4fbd17 --- /dev/null +++ b/contracts/interfaces/IControllerV2.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {Epoch} from "../libraries/Epoch.sol"; +import {BookId} from "../external/clober-v2/BookId.sol"; +import {Coupon} from "../libraries/Coupon.sol"; + +interface IControllerV2 { + event SetCouponMarket(address indexed asset, Epoch indexed epoch, BookId sellMarketBookId, BookId buyMarketBookId); + event CouponTrade(uint256 indexed positionId, int256 cost, Coupon[] couponsToBuy, Coupon[] couponsToSell); + + error InvalidAccess(); + error InvalidMarket(); + error ControllerSlippage(); +} diff --git a/contracts/interfaces/IDepositControllerV2.sol b/contracts/interfaces/IDepositControllerV2.sol new file mode 100644 index 0000000..7eb6155 --- /dev/null +++ b/contracts/interfaces/IDepositControllerV2.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IControllerV2} from "./IControllerV2.sol"; +import {ERC20PermitParams, PermitSignature} from "../libraries/PermitParams.sol"; +import {Epoch} from "../libraries/Epoch.sol"; + +interface IDepositControllerV2 is IControllerV2 { + function deposit( + address token, + uint256 amount, + Epoch expiredWith, + int256 minEarnInterest, + ERC20PermitParams calldata tokenPermitParams + ) external payable returns (uint256 positionId); + + function adjust( + uint256 positionId, + uint256 amount, + Epoch expiredWith, + int256 interestThreshold, + ERC20PermitParams calldata tokenPermitParams, + PermitSignature calldata positionPermitParams + ) external payable; +} diff --git a/contracts/libraries/Controller.sol b/contracts/libraries/Controller.sol index 0ddb709..48299b4 100644 --- a/contracts/libraries/Controller.sol +++ b/contracts/libraries/Controller.sol @@ -23,6 +23,7 @@ import {Wrapped1155MetadataBuilder} from "./Wrapped1155MetadataBuilder.sol"; import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; import {ISubstitute} from "../interfaces/ISubstitute.sol"; import {IController} from "../interfaces/IController.sol"; +import {SubstituteLibrary} from "../libraries/Substitute.sol"; import {ReentrancyGuard} from "./ReentrancyGuard.sol"; import {SubstituteLibrary} from "./Substitute.sol"; @@ -108,7 +109,7 @@ abstract contract Controller is market.marketOrder(address(this), 0, 0, lastCoupon.amount, 2, data); } else { if (remainingInterest < 0) revert ControllerSlippage(); - ISubstitute(token).ensureBalance(user, amountToPay); + _mintSubstituteAll(token, user, amountToPay); } } @@ -154,6 +155,10 @@ abstract contract Controller is return ISubstitute(substitute).underlyingToken(); } + function _mintSubstituteAll(address token, address user, uint256 minRequired) internal { + ISubstitute(token).mintAll(user, minRequired); + } + function _wrapCoupons(Coupon[] memory coupons) internal { // wrap 1155 to 20 bytes memory metadata = Wrapped1155MetadataBuilder.buildWrapped1155BatchMetadata(coupons); diff --git a/contracts/libraries/ControllerV2.sol b/contracts/libraries/ControllerV2.sol new file mode 100644 index 0000000..28fe6d8 --- /dev/null +++ b/contracts/libraries/ControllerV2.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: - +// License: https://license.coupon.finance/LICENSE.pdf + +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {CloberMarketSwapCallbackReceiver} from "../external/clober/CloberMarketSwapCallbackReceiver.sol"; +import {IWETH9} from "../external/weth/IWETH9.sol"; +import {IWrapped1155Factory} from "../external/wrapped1155/IWrapped1155Factory.sol"; +import {CloberOrderBook} from "../external/clober/CloberOrderBook.sol"; +import {ICouponManager} from "../interfaces/ICouponManager.sol"; +import {Coupon, CouponLibrary} from "./Coupon.sol"; +import {CouponKey, CouponKeyLibrary} from "./CouponKey.sol"; +import {Wrapped1155MetadataBuilder} from "./Wrapped1155MetadataBuilder.sol"; +import {IERC721Permit} from "../interfaces/IERC721Permit.sol"; +import {ISubstitute} from "../interfaces/ISubstitute.sol"; +import {ReentrancyGuard} from "./ReentrancyGuard.sol"; +import {IController} from "../external/clober-v2/IController.sol"; +import {IBookManager} from "../external/clober-v2/IBookManager.sol"; +import {BookId, BookIdLibrary} from "../external/clober-v2/BookId.sol"; +import {CurrencyLibrary, Currency} from "../external/clober-v2/Currency.sol"; +import {IControllerV2} from "../interfaces/IControllerV2.sol"; +import {SubstituteLibrary} from "./Substitute.sol"; + +import {Epoch} from "./Epoch.sol"; + +abstract contract ControllerV2 is IControllerV2, ERC1155Holder, Ownable2Step, ReentrancyGuard { + using SafeCast for uint256; + using BookIdLibrary for IBookManager.BookKey; + using SafeERC20 for IERC20; + using CouponKeyLibrary for CouponKey; + using CouponLibrary for Coupon; + using CurrencyLibrary for Currency; + using SubstituteLibrary for ISubstitute; + + IWrapped1155Factory internal immutable _wrapped1155Factory; + IController internal immutable _cloberController; + ICouponManager internal immutable _couponManager; + IBookManager internal immutable _bookManager; + IWETH9 internal immutable _weth; + + mapping(uint256 couponId => IBookManager.BookKey) internal _couponSellMarkets; + mapping(uint256 couponId => IBookManager.BookKey) internal _couponBuyMarkets; + + constructor( + address wrapped1155Factory, + address cloberController, + address bookManager, + address couponManager, + address weth + ) Ownable(msg.sender) { + _wrapped1155Factory = IWrapped1155Factory(wrapped1155Factory); + _cloberController = IController(cloberController); + _couponManager = ICouponManager(couponManager); + _bookManager = IBookManager(bookManager); + + _couponManager.setApprovalForAll(address(_cloberController), true); + _weth = IWETH9(weth); + } + + modifier wrapAndRefundETH() { + bool hasMsgValue = address(this).balance > 0; + if (hasMsgValue) _weth.deposit{value: address(this).balance}(); + _; + if (hasMsgValue) { + uint256 leftBalance = _weth.balanceOf(address(this)); + if (leftBalance > 0) { + _weth.withdraw(leftBalance); + (bool success,) = msg.sender.call{value: leftBalance}(""); + require(success); + } + } + } + + function _executeCouponTrade( + address user, + uint256 positionId, + address token, + Coupon[] memory couponsToMint, + Coupon[] memory couponsToBurn, + int256 interestThreshold + ) internal { + uint256 length = couponsToBurn.length + couponsToMint.length; + IController.Action[] memory actionList = new IController.Action[](length); + bytes[] memory paramsDataList = new bytes[](length); + address[] memory tokensToSettle = new address[](length + 1); + tokensToSettle[length] = token; + + uint256 amount; + length = couponsToBurn.length; + for (uint256 i = 0; i < length; ++i) { + actionList[i] = IController.Action.TAKE; + IBookManager.BookKey memory key = _couponBuyMarkets[couponsToBurn[i].key.toId()]; + tokensToSettle[i] = Currency.unwrap(key.quote); + amount += couponsToBurn[i].amount; + paramsDataList[i] = abi.encode( + IController.TakeOrderParams({ + id: key.toId(), + limitPrice: 0, + quoteAmount: couponsToBurn[i].amount, + hookData: "" + }) + ); + } + if (amount > 0) IERC20(token).approve(address(_cloberController), amount); + + length = couponsToMint.length; + for (uint256 i = 0; i < length; ++i) { + actionList[couponsToBurn.length + i] = IController.Action.SPEND; + IBookManager.BookKey memory key = _couponSellMarkets[couponsToMint[i].key.toId()]; + tokensToSettle[couponsToBurn.length + i] = Currency.unwrap(key.base); + amount = couponsToMint[i].amount; + paramsDataList[couponsToBurn.length + i] = abi.encode( + IController.SpendOrderParams({id: key.toId(), limitPrice: 0, baseAmount: amount, hookData: ""}) + ); + // key.base can't be Currency.NATIVE + IERC20(Currency.unwrap(key.base)).approve(address(_cloberController), amount); + } + + if (interestThreshold > 0) { + if (IERC20(token).balanceOf(address(this)) < uint256(interestThreshold)) { + address underlyingToken = ISubstitute(token).underlyingToken(); + amount = Math.min( + IERC20(underlyingToken).allowance(user, address(this)), IERC20(underlyingToken).balanceOf(user) + ); + ISubstitute(token).mintAll(user, Math.min(uint256(interestThreshold), amount)); + } + IERC20(token).approve(address(_cloberController), uint256(interestThreshold)); + } + + uint256 beforeBalance = IERC20(token).balanceOf(address(this)); + int256 balanceDiff; + unchecked { + IController.ERC20PermitParams[] memory erc20PermitParamsList; + IController.ERC721PermitParams[] memory erc721PermitParamsList; + _cloberController.execute( + actionList, + paramsDataList, + tokensToSettle, + erc20PermitParamsList, + erc721PermitParamsList, + uint64(block.timestamp) + ); + if (interestThreshold > 0) { + IERC20(token).approve(address(_cloberController), 0); + } + + uint256 afterBalance = IERC20(token).balanceOf(address(this)); + if (afterBalance > beforeBalance) { + balanceDiff = -(afterBalance - beforeBalance).toInt256(); + } else { + balanceDiff = (beforeBalance - afterBalance).toInt256(); + } + } + if (interestThreshold < balanceDiff) { + revert ControllerSlippage(); + } + emit CouponTrade(positionId, balanceDiff, couponsToBurn, couponsToMint); + } + + function _getUnderlyingToken(address substitute) internal view returns (address) { + return ISubstitute(substitute).underlyingToken(); + } + + function _burnAllSubstitute(address substitute, address to) internal { + uint256 leftAmount = IERC20(substitute).balanceOf(address(this)); + if (leftAmount == 0) return; + ISubstitute(substitute).burn(leftAmount, to); + } + + function _mintSubstituteAll(address token, address user, uint256 minRequired) internal { + ISubstitute(token).mintAll(user, minRequired); + } + + function _wrapCoupons(Coupon[] memory coupons) internal { + // wrap 1155 to 20 + bytes memory metadata = Wrapped1155MetadataBuilder.buildWrapped1155BatchMetadata(coupons); + _couponManager.safeBatchTransferFrom(address(this), address(_wrapped1155Factory), coupons, metadata); + } + + function _unwrapCoupons(Coupon[] memory coupons) internal { + uint256[] memory tokenIds = new uint256[](coupons.length); + uint256[] memory amounts = new uint256[](coupons.length); + unchecked { + for (uint256 i = 0; i < coupons.length; ++i) { + tokenIds[i] = coupons[i].id(); + amounts[i] = coupons[i].amount; + } + } + bytes memory metadata = Wrapped1155MetadataBuilder.buildWrapped1155BatchMetadata(coupons); + _wrapped1155Factory.batchUnwrap(address(_couponManager), tokenIds, amounts, address(this), metadata); + } + + function getCouponMarket(CouponKey memory couponKey) + external + view + returns (IBookManager.BookKey memory, IBookManager.BookKey memory) + { + return (_couponSellMarkets[couponKey.toId()], _couponBuyMarkets[couponKey.toId()]); + } + + function setCouponBookKey( + CouponKey memory couponKey, + IBookManager.BookKey calldata sellBookKey, + IBookManager.BookKey calldata buyBookKey + ) public virtual onlyOwner { + bytes memory metadata = Wrapped1155MetadataBuilder.buildWrapped1155Metadata(couponKey); + uint256 couponId = couponKey.toId(); + address wrappedCoupon = _wrapped1155Factory.getWrapped1155(address(_couponManager), couponId, metadata); + + BookId sellMarketBookId = sellBookKey.toId(); + BookId buyMarketBookId = buyBookKey.toId(); + if ( + _bookManager.getBookKey(sellMarketBookId).unit != sellBookKey.unit + || _bookManager.getBookKey(buyMarketBookId).unit != buyBookKey.unit + || Currency.unwrap(sellBookKey.quote) != couponKey.asset + || Currency.unwrap(sellBookKey.base) != wrappedCoupon || Currency.unwrap(buyBookKey.quote) != wrappedCoupon + || Currency.unwrap(buyBookKey.base) != couponKey.asset + ) { + revert InvalidMarket(); + } + + _couponSellMarkets[couponId] = sellBookKey; + _couponBuyMarkets[couponId] = buyBookKey; + + emit SetCouponMarket(couponKey.asset, couponKey.epoch, sellMarketBookId, buyMarketBookId); + } + + receive() external payable {} +} diff --git a/contracts/libraries/Substitute.sol b/contracts/libraries/Substitute.sol index 46d1daf..1600591 100644 --- a/contracts/libraries/Substitute.sol +++ b/contracts/libraries/Substitute.sol @@ -10,21 +10,22 @@ import {ISubstitute} from "../interfaces/ISubstitute.sol"; library SubstituteLibrary { using SafeERC20 for IERC20; - function ensureBalance(ISubstitute substitute, address payer, uint256 amount) internal { - uint256 balance = IERC20(address(substitute)).balanceOf(address(this)); - if (balance >= amount) { - return; - } + function mintAll(ISubstitute substitute, address payer, uint256 minRequiredBalance) internal { address underlyingToken = substitute.underlyingToken(); + uint256 thisBalance = IERC20(address(substitute)).balanceOf(address(this)); uint256 underlyingBalance = IERC20(underlyingToken).balanceOf(address(this)); - unchecked { - amount -= balance; - if (underlyingBalance < amount) { - IERC20(underlyingToken).safeTransferFrom(payer, address(this), amount - underlyingBalance); + if (minRequiredBalance > thisBalance + underlyingBalance) { + unchecked { + IERC20(underlyingToken).safeTransferFrom( + payer, address(this), minRequiredBalance - thisBalance - underlyingBalance + ); + underlyingBalance = minRequiredBalance - thisBalance; } } - IERC20(underlyingToken).approve(address(substitute), amount); - substitute.mint(amount, address(this)); + if (underlyingBalance > 0) { + IERC20(underlyingToken).approve(address(substitute), underlyingBalance); + substitute.mint(underlyingBalance, address(this)); + } } function burnAll(ISubstitute substitute, address to) internal { diff --git a/test/foundry/Constants.sol b/test/foundry/Constants.sol index 8e17ad2..9d4eef8 100644 --- a/test/foundry/Constants.sol +++ b/test/foundry/Constants.sol @@ -25,6 +25,8 @@ library Constants { address internal constant COUPON_BOND_POSITION_MANAGER = 0x0Cf91Bc7a67B063142C029a69fF9C8ccd93476E2; address internal constant COUPON_LOAN_POSITION_MANAGER = 0x03d65411684ae7B5440E11a6063881a774C733dF; address internal constant COUPON_COUPON_MANAGER = 0x8bbcA766D175aDbffB073832262990df1c5ef748; + address internal constant CLOBER_CONTROLLER = 0xC54F2c6bDCa8fA703fF90A7710CCd95d830d5b43; + address internal constant CLOBER_BOOK_MANAGER = 0x90aF32914eCA3AA9a4F76B53e4d82363a547B9DC; address internal constant COUPON_ORACLE = 0xF8e9ab02b057978c29Ca57c7E086D46983764A13; address internal constant COUPON_WETH_SUBSTITUTE = 0xAb6c37355D6C06fcF73Ab0E049d9Cf922f297573; address internal constant COUPON_USDC_SUBSTITUTE = 0x7Ed1145045c8B754506d375Cdf90734550d1077e; diff --git a/test/foundry/integration/BorrowControllerV2.t.sol b/test/foundry/integration/BorrowControllerV2.t.sol new file mode 100644 index 0000000..5a4146d --- /dev/null +++ b/test/foundry/integration/BorrowControllerV2.t.sol @@ -0,0 +1,862 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {Constants} from "../Constants.sol"; +import {ForkUtils, ERC20Utils, Utils, PermitSignLibrary} from "../Utils.sol"; +import {IAssetPool} from "../../../contracts/interfaces/IAssetPool.sol"; +import {IAaveTokenSubstitute} from "../../../contracts/interfaces/IAaveTokenSubstitute.sol"; +import {ICouponOracle} from "../../../contracts/interfaces/ICouponOracle.sol"; +import {ICouponManager} from "../../../contracts/interfaces/ICouponManager.sol"; +import {ILoanPositionManager, ILoanPositionManagerTypes} from "../../../contracts/interfaces/ILoanPositionManager.sol"; +import {Coupon, CouponLibrary} from "../../../contracts/libraries/Coupon.sol"; +import {CouponKey, CouponKeyLibrary} from "../../../contracts/libraries/CouponKey.sol"; +import {Epoch, EpochLibrary} from "../../../contracts/libraries/Epoch.sol"; +import {LoanPosition} from "../../../contracts/libraries/LoanPosition.sol"; +import {Wrapped1155MetadataBuilder} from "../../../contracts/libraries/Wrapped1155MetadataBuilder.sol"; +import {ERC20PermitParams, PermitSignature} from "../../../contracts/libraries/PermitParams.sol"; +import {IWrapped1155Factory} from "../../../contracts/external/wrapped1155/IWrapped1155Factory.sol"; +import {CloberOrderBook} from "../../../contracts/external/clober/CloberOrderBook.sol"; +import {BorrowControllerV2} from "../../../contracts/BorrowControllerV2.sol"; +import {Tick} from "../../../contracts/external/clober-v2/Tick.sol"; +import {BookIdLibrary} from "../../../contracts/external/clober-v2/BookId.sol"; +import {IBorrowControllerV2} from "../../../contracts/interfaces/IBorrowControllerV2.sol"; +import {IControllerV2} from "../../../contracts/interfaces/IControllerV2.sol"; +import {IController} from "../../../contracts/external/clober-v2/IController.sol"; +import {IBookManager} from "../../../contracts/external/clober-v2/IBookManager.sol"; +import {IHooks} from "../../../contracts/external/clober-v2/IHooks.sol"; +import {Currency, CurrencyLibrary} from "../../../contracts/external/clober-v2/Currency.sol"; +import {FeePolicy, FeePolicyLibrary} from "../../../contracts/external/clober-v2/FeePolicy.sol"; +import {DepositControllerV2} from "../../../contracts/DepositControllerV2.sol"; +import {MockBookManager} from "../mocks/MockBookManager.sol"; +import {MockController} from "../mocks/MockController.sol"; +import {AaveTokenSubstitute} from "../../../contracts/AaveTokenSubstitute.sol"; + +contract BorrowControllerV2IntegrationTest is Test, ERC1155Holder { + using Strings for *; + using ERC20Utils for IERC20; + using CouponKeyLibrary for CouponKey; + using EpochLibrary for Epoch; + using PermitSignLibrary for Vm; + using BookIdLibrary for IBookManager.BookKey; + + address public constant MARKET_MAKER = address(999123); + + IAssetPool public assetPool; + BorrowControllerV2 public borrowController; + ILoanPositionManager public loanPositionManager; + IWrapped1155Factory public wrapped1155Factory; + ICouponManager public couponManager; + ICouponOracle public oracle; + IController public cloberController; + IBookManager public bookManager; + IERC20 public usdc; + IERC20 public weth; + address public wausdc; + address public waweth; + address public user; + ERC20PermitParams public emptyERC20PermitParams; + PermitSignature public emptyERC721PermitParams; + + CouponKey[] public couponKeys; + address[] public wrappedCoupons; + + function setUp() public { + ForkUtils.fork(vm, 192621731); + user = vm.addr(1); + + usdc = IERC20(Constants.USDC); + weth = IERC20(Constants.WETH); + vm.startPrank(Constants.USDC_WHALE); + usdc.transfer(user, usdc.amount(1_000_000)); + usdc.transfer(address(this), usdc.amount(1_000_000)); + vm.stopPrank(); + vm.deal(user, 1_000_000 ether); + + bool success; + vm.startPrank(user); + (success,) = payable(address(weth)).call{value: 500_000 ether}(""); + require(success, "transfer failed"); + vm.stopPrank(); + + vm.deal(address(this), 1_000_000 ether); + (success,) = payable(address(weth)).call{value: 500_000 ether}(""); + require(success, "transfer failed"); + + wrapped1155Factory = IWrapped1155Factory(Constants.WRAPPED1155_FACTORY); + wausdc = Constants.COUPON_USDC_SUBSTITUTE; + waweth = Constants.COUPON_WETH_SUBSTITUTE; + oracle = ICouponOracle(Constants.COUPON_COUPON_MANAGER); + assetPool = IAssetPool(Constants.COUPON_ASSET_POOL); + couponManager = ICouponManager(Constants.COUPON_COUPON_MANAGER); + loanPositionManager = ILoanPositionManager(Constants.COUPON_LOAN_POSITION_MANAGER); + bookManager = IBookManager(Constants.CLOBER_BOOK_MANAGER); + cloberController = IController(Constants.CLOBER_CONTROLLER); + + borrowController = new BorrowControllerV2( + Constants.WRAPPED1155_FACTORY, + address(cloberController), + address(bookManager), + address(couponManager), + Constants.WETH, + address(loanPositionManager), + Constants.ODOS_V2_SWAP_ROUTER + ); + + usdc.approve(wausdc, usdc.amount(3_000)); + IAaveTokenSubstitute(wausdc).mint(usdc.amount(3_000), address(this)); + IERC20(Constants.WETH).approve(waweth, 3_000 ether); + IAaveTokenSubstitute(waweth).mint(3_000 ether, address(this)); + + // create wrapped1155 + couponKeys.push(CouponKey({asset: wausdc, epoch: EpochLibrary.current()})); + couponKeys.push(CouponKey({asset: waweth, epoch: EpochLibrary.current()})); + couponKeys.push(CouponKey({asset: wausdc, epoch: EpochLibrary.current().add(1)})); + couponKeys.push(CouponKey({asset: waweth, epoch: EpochLibrary.current().add(1)})); + + IController.MakeOrderParams[] memory makeOrderParamsList = new IController.MakeOrderParams[](8); + address[] memory tokensToSettle = new address[](6); + for (uint256 i = 0; i < 4; i++) { + CouponKey memory key = couponKeys[i]; + IController.OpenBookParams[] memory openBookParamsList = new IController.OpenBookParams[](2); + address wrappedToken = wrapped1155Factory.requireWrapped1155( + address(couponManager), key.toId(), Wrapped1155MetadataBuilder.buildWrapped1155Metadata(couponKeys[i]) + ); + wrappedCoupons.push(wrappedToken); + IHooks hooks; + IBookManager.BookKey memory sellBookKey = IBookManager.BookKey({ + base: Currency.wrap(wrappedToken), + unit: i % 2 == 0 ? 1 : 10 ** 6, + quote: Currency.wrap(i % 2 == 0 ? wausdc : waweth), + makerPolicy: FeePolicyLibrary.encode(true, 100), + hooks: hooks, + takerPolicy: FeePolicyLibrary.encode(true, 100) + }); + IBookManager.BookKey memory buyBookKey = IBookManager.BookKey({ + base: Currency.wrap(i % 2 == 0 ? wausdc : waweth), + unit: i % 2 == 0 ? 1 : 10 ** 6, + quote: Currency.wrap(wrappedCoupons[i]), + makerPolicy: FeePolicyLibrary.encode(true, 100), + hooks: hooks, + takerPolicy: FeePolicyLibrary.encode(true, 100) + }); + + openBookParamsList[0] = IController.OpenBookParams({key: sellBookKey, hookData: ""}); + openBookParamsList[1] = IController.OpenBookParams({key: buyBookKey, hookData: ""}); + cloberController.open(openBookParamsList, uint64(block.timestamp)); + borrowController.setCouponBookKey(key, sellBookKey, buyBookKey); + + uint256 amount = IERC20(wrappedToken).amount(1600); + Coupon[] memory coupons = Utils.toArr(Coupon(key, amount)); + vm.prank(Constants.COUPON_LOAN_POSITION_MANAGER); + couponManager.mintBatch(address(this), coupons, ""); + couponManager.safeBatchTransferFrom( + address(this), + address(wrapped1155Factory), + coupons, + Wrapped1155MetadataBuilder.buildWrapped1155Metadata(couponKeys[i]) + ); + + makeOrderParamsList[i * 2] = IController.MakeOrderParams({ + id: sellBookKey.toId(), + tick: Tick.wrap(-39122), + quoteAmount: i % 2 == 0 ? IERC20(wausdc).amount(1200) : IERC20(waweth).amount(1200), + hookData: "" + }); + makeOrderParamsList[i * 2 + 1] = IController.MakeOrderParams({ + id: buyBookKey.toId(), + tick: Tick.wrap(39122), + quoteAmount: IERC20(wrappedToken).amount(1200), + hookData: "" + }); + tokensToSettle[i] = wrappedToken; + IERC20(wrappedToken).approve(address(cloberController), type(uint256).max); + } + + IERC20(wausdc).approve(address(cloberController), type(uint256).max); + IERC20(waweth).approve(address(cloberController), type(uint256).max); + tokensToSettle[4] = address(wausdc); + tokensToSettle[5] = address(waweth); + IController.ERC20PermitParams[] memory permitParamsList; + cloberController.make(makeOrderParamsList, tokensToSettle, permitParamsList, uint64(block.timestamp)); + + vm.prank(Constants.USDC_WHALE); + usdc.transfer(user, usdc.amount(10_000)); + vm.deal(address(user), 100 ether); + } + + function _initialBorrow( + address borrower, + address collateralToken, + address borrowToken, + uint256 collateralAmount, + uint256 debtAmount, + uint16 loanEpochs + ) internal returns (uint256 positionId) { + positionId = loanPositionManager.nextId(); + ERC20PermitParams memory permitParams = vm.signPermit( + 1, + IERC20Permit(IAaveTokenSubstitute(collateralToken).underlyingToken()), + address(borrowController), + collateralAmount + ); + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(borrower); + borrowController.borrow( + collateralToken, + borrowToken, + collateralAmount, + debtAmount, + type(int256).max, + EpochLibrary.current().add(loanEpochs - 1), + swapParams, + permitParams + ); + } + + function testBorrow() public { + uint256 collateralAmount = usdc.amount(10000); + uint256 debtAmount = 1 ether; + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + + uint256 positionId = _initialBorrow(user, wausdc, waweth, collateralAmount, debtAmount, 1); + LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId); + + uint256 couponAmount = 0.0201 ether; + + assertEq(loanPositionManager.ownerOf(positionId), user, "POSITION_OWNER"); + assertEq(usdc.balanceOf(user), beforeUSDCBalance - collateralAmount, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance + debtAmount - couponAmount, "NATIVE_BALANCE_GE"); + assertLe(user.balance, beforeETHBalance + debtAmount - couponAmount + 0.001 ether, "NATIVE_BALANCE_LE"); + assertEq(loanPosition.expiredWith, EpochLibrary.current(), "POSITION_EXPIRE_EPOCH"); + assertEq(loanPosition.collateralAmount, collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(loanPosition.debtAmount, debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(loanPosition.collateralToken, wausdc, "POSITION_COLLATERAL_TOKEN"); + assertEq(loanPosition.debtToken, waweth, "POSITION_DEBT_TOKEN"); + } + + function testBorrowMore() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount, + beforeLoanPosition.debtAmount + 0.5 ether, + type(int256).max, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + uint256 borrowMoreAmount = 0.5 ether; + uint256 couponAmount = 0.0101 ether; + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance + borrowMoreAmount - couponAmount, "NATIVE_BALANCE_GE"); + assertLe(user.balance, beforeETHBalance + borrowMoreAmount - couponAmount + 0.001 ether, "NATIVE_BALANCE_LE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq(beforeLoanPosition.collateralAmount, afterLoanPosition.collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(beforeLoanPosition.debtAmount + borrowMoreAmount, afterLoanPosition.debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testAddCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(123); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + ERC20PermitParams memory permit20Params = vm.signPermit( + 1, IERC20Permit(IAaveTokenSubstitute(wausdc).underlyingToken()), address(borrowController), collateralAmount + ); + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount + collateralAmount, + beforeLoanPosition.debtAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + permit20Params, + emptyERC20PermitParams + ); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance - collateralAmount, "USDC_BALANCE"); + assertEq(user.balance, beforeETHBalance, "NATIVE_BALANCE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq( + beforeLoanPosition.collateralAmount + collateralAmount, + afterLoanPosition.collateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertEq(beforeLoanPosition.debtAmount, afterLoanPosition.debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testRemoveCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(123); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + beforeLoanPosition.debtAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance + collateralAmount, "USDC_BALANCE"); + assertEq(user.balance, beforeETHBalance, "NATIVE_BALANCE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq( + beforeLoanPosition.collateralAmount - collateralAmount, + afterLoanPosition.collateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertEq(beforeLoanPosition.debtAmount, afterLoanPosition.debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testExtendLoanDuration() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeWETHBalance = weth.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint16 epochs = 1; + uint256 maxPayInterest = 0.0201 ether * uint256(epochs); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.startPrank(user); + weth.approve(address(borrowController), maxPayInterest); + borrowController.adjust{value: maxPayInterest}( + positionId, + beforeLoanPosition.collateralAmount, + beforeLoanPosition.debtAmount, + int256(maxPayInterest), + beforeLoanPosition.expiredWith.add(epochs), + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + vm.stopPrank(); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertGe( + user.balance + weth.balanceOf(user) - beforeWETHBalance, + beforeETHBalance - maxPayInterest, + "NATIVE_BALANCE_GE" + ); + assertLe( + user.balance + weth.balanceOf(user) - beforeWETHBalance, + beforeETHBalance - maxPayInterest + 0.0001 ether, + "NATIVE_BALANCE_LE" + ); + assertEq(beforeLoanPosition.expiredWith.add(epochs), afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq(beforeLoanPosition.collateralAmount, afterLoanPosition.collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(beforeLoanPosition.debtAmount, afterLoanPosition.debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testShortenLoanDuration() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint16 epochs = 1; + uint256 minEarnInterest = 0.0199 ether * epochs; + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount, + beforeLoanPosition.debtAmount, + 0, + beforeLoanPosition.expiredWith.sub(epochs), + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance + minEarnInterest, "NATIVE_BALANCE_GE"); + assertLe(user.balance, beforeETHBalance + minEarnInterest + 0.01 ether, "NATIVE_BALANCE_LE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith.add(epochs), "POSITION_EXPIRE_EPOCH"); + assertEq(beforeLoanPosition.collateralAmount, afterLoanPosition.collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(beforeLoanPosition.debtAmount, afterLoanPosition.debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testRepay() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 repayAmount = 0.3 ether; + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + ERC20PermitParams memory permit20Params = vm.signPermit( + 1, IERC20Permit(IAaveTokenSubstitute(waweth).underlyingToken()), address(borrowController), repayAmount + ); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + borrowController.adjust{value: repayAmount}( + positionId, + beforeLoanPosition.collateralAmount, + beforeLoanPosition.debtAmount - repayAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + permit20Params + ); + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + uint256 couponAmount = 0.006 ether; + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertLe(user.balance, beforeETHBalance - repayAmount + couponAmount, "NATIVE_BALANCE_GE"); + assertGe(user.balance, beforeETHBalance - repayAmount + couponAmount - 0.0001 ether, "NATIVE_BALANCE_LE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq(beforeLoanPosition.collateralAmount, afterLoanPosition.collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(beforeLoanPosition.debtAmount, afterLoanPosition.debtAmount + repayAmount, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testLeverage() public { + uint256 collateralAmount = 0.4 ether; + uint256 debtAmount = usdc.amount(550); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeWETHBalance = weth.balanceOf(user); + uint256 beforeETHBalance = user.balance; + + uint256 positionId = loanPositionManager.nextId(); + ERC20PermitParams memory permitParams = vm.signPermit( + 1, + IERC20Permit(AaveTokenSubstitute(payable(wausdc)).underlyingToken()), + address(borrowController), + collateralAmount + ); + + IBorrowControllerV2.SwapParams memory swapParams; + swapParams.data = fromHex( + string.concat( + "83bd37f9000a000b041dcd65000801f447c4595e16b000068d00019b57DcA972Db5D8866c630554AcdbDfE58b2659c00000001", + this.remove0x(Strings.toHexString(address(borrowController))), + "000000010803020801b9269d0f2801010102011dc097350301010103010019011fe3097c0b010104010001240149360b0101050100012fba5f510b0101060100000b0101070100ff0000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab1db86e7fe4074e3c29d2fd0ed1d104c00e11a196b4ef385d20247f5d0f9cd2e3f716b9a55e71df9347fcdc35463e3770c2fb992716cd070b63540b947f6416e1ed89a3abca179f971b30555eb2234f30c6c9ab1c1dc392b53f9fb2ea6d9dace5f99efdc480000000000000000000000000000000000000000" + ) + ); + swapParams.amount = usdc.amount(500); + swapParams.inSubstitute = address(wausdc); + + vm.prank(user); + borrowController.borrow{value: 0.26 ether}( + waweth, + wausdc, + collateralAmount, + debtAmount, + type(int256).max, + EpochLibrary.current(), + swapParams, + permitParams + ); + + LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId); + + assertEq(loanPositionManager.ownerOf(positionId), user, "POSITION_OWNER"); + assertGt(usdc.balanceOf(user) - beforeUSDCBalance, 0, "USDC_BALANCE"); + assertLt(beforeETHBalance - user.balance, 0.26 ether, "NATIVE_BALANCE"); + assertEq(beforeWETHBalance, weth.balanceOf(user), "WETH_BALANCE"); + assertEq(loanPosition.expiredWith, EpochLibrary.current(), "POSITION_EXPIRE_EPOCH"); + assertEq(loanPosition.collateralAmount, collateralAmount, "POSITION_COLLATERAL_AMOUNT"); + assertEq(loanPosition.debtAmount, debtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(loanPosition.collateralToken, waweth, "POSITION_COLLATERAL_TOKEN"); + assertEq(loanPosition.debtToken, wausdc, "POSITION_DEBT_TOKEN"); + } + + function testLeverageMore() public { + uint256 positionId = _initialBorrow(user, waweth, wausdc, 1 ether, usdc.amount(500), 1); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeWETHBalance = weth.balanceOf(user); + uint256 beforeETHBalance = user.balance; + + LoanPosition memory loanPosition = loanPositionManager.getPosition(positionId); + + uint256 beforePositionCollateralAmount = loanPosition.collateralAmount; + uint256 beforePositionDebtAmount = loanPosition.debtAmount; + + uint256 collateralAmount = 0.4 ether; + uint256 debtAmount = usdc.amount(550); + + ERC20PermitParams memory permitParams = vm.signPermit( + 1, + IERC20Permit(AaveTokenSubstitute(payable(wausdc)).underlyingToken()), + address(borrowController), + collateralAmount + ); + + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + swapParams.data = fromHex( + string.concat( + "83bd37f9000a000b041dcd65000801f447c4595e16b000068d00019b57DcA972Db5D8866c630554AcdbDfE58b2659c00000001", + this.remove0x(Strings.toHexString(address(borrowController))), + "000000010803020801b9269d0f2801010102011dc097350301010103010019011fe3097c0b010104010001240149360b0101050100012fba5f510b0101060100000b0101070100ff0000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab1db86e7fe4074e3c29d2fd0ed1d104c00e11a196b4ef385d20247f5d0f9cd2e3f716b9a55e71df9347fcdc35463e3770c2fb992716cd070b63540b947f6416e1ed89a3abca179f971b30555eb2234f30c6c9ab1c1dc392b53f9fb2ea6d9dace5f99efdc480000000000000000000000000000000000000000" + ) + ); + swapParams.amount = usdc.amount(500); + swapParams.inSubstitute = address(wausdc); + + vm.prank(user); + borrowController.adjust{value: 0.26 ether}( + positionId, + loanPosition.collateralAmount + collateralAmount, + loanPosition.debtAmount + debtAmount, + type(int256).max, + loanPosition.expiredWith, + swapParams, + permit721Params, + permitParams, + emptyERC20PermitParams + ); + + loanPosition = loanPositionManager.getPosition(positionId); + + assertEq(loanPositionManager.ownerOf(positionId), user, "POSITION_OWNER"); + assertGt(usdc.balanceOf(user) - beforeUSDCBalance, 0, "USDC_BALANCE"); + assertLt(beforeETHBalance - user.balance, 0.26 ether, "NATIVE_BALANCE"); + assertEq(beforeWETHBalance, weth.balanceOf(user), "WETH_BALANCE"); + assertEq(loanPosition.expiredWith, EpochLibrary.current(), "POSITION_EXPIRE_EPOCH"); + assertEq( + loanPosition.collateralAmount, + collateralAmount + beforePositionCollateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertEq(loanPosition.debtAmount, debtAmount + beforePositionDebtAmount, "POSITION_DEBT_AMOUNT"); + assertEq(loanPosition.collateralToken, waweth, "POSITION_COLLATERAL_TOKEN"); + assertEq(loanPosition.debtToken, wausdc, "POSITION_DEBT_TOKEN"); + } + + function testRepayWithCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(500); + uint256 debtAmount = 0.86 ether; + + IBorrowControllerV2.SwapParams memory swapParams; + swapParams.data = fromHex( + string.concat( + "83bd37f9000a000b041dcd65000801f447c4595e16b000068d00019b57DcA972Db5D8866c630554AcdbDfE58b2659c00000001", + this.remove0x(Strings.toHexString(address(borrowController))), + "000000010803020801b9269d0f2801010102011dc097350301010103010019011fe3097c0b010104010001240149360b0101050100012fba5f510b0101060100000b0101070100ff0000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab1db86e7fe4074e3c29d2fd0ed1d104c00e11a196b4ef385d20247f5d0f9cd2e3f716b9a55e71df9347fcdc35463e3770c2fb992716cd070b63540b947f6416e1ed89a3abca179f971b30555eb2234f30c6c9ab1c1dc392b53f9fb2ea6d9dace5f99efdc480000000000000000000000000000000000000000" + ) + ); + swapParams.amount = usdc.amount(500); + swapParams.inSubstitute = address(wausdc); + + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + debtAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance, "NATIVE_BALANCE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq( + beforeLoanPosition.collateralAmount - collateralAmount, + afterLoanPosition.collateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertLe(afterLoanPosition.debtAmount, debtAmount, "POSITION_DEBT_AMOUNT"); + assertGe(afterLoanPosition.debtAmount, 0.7 ether, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testRepayAllWithCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 0.14 ether, 2); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(500); + + IBorrowControllerV2.SwapParams memory swapParams; + swapParams.data = fromHex( + string.concat( + "83bd37f9000a000b041dcd65000801f447c4595e16b000068d00019b57DcA972Db5D8866c630554AcdbDfE58b2659c00000001", + this.remove0x(Strings.toHexString(address(borrowController))), + "000000010803020801b9269d0f2801010102011dc097350301010103010019011fe3097c0b010104010001240149360b0101050100012fba5f510b0101060100000b0101070100ff0000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab1db86e7fe4074e3c29d2fd0ed1d104c00e11a196b4ef385d20247f5d0f9cd2e3f716b9a55e71df9347fcdc35463e3770c2fb992716cd070b63540b947f6416e1ed89a3abca179f971b30555eb2234f30c6c9ab1c1dc392b53f9fb2ea6d9dace5f99efdc480000000000000000000000000000000000000000" + ) + ); + swapParams.amount = usdc.amount(500); + swapParams.inSubstitute = address(wausdc); + + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + 0, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance, "NATIVE_BALANCE"); + assertEq(Epoch.wrap(649), afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq( + beforeLoanPosition.collateralAmount - usdc.amount(500), + afterLoanPosition.collateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertEq(afterLoanPosition.debtAmount, 0, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testRepayWithLeftCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + + uint256 beforeUSDCBalance = usdc.balanceOf(user); + uint256 beforeETHBalance = user.balance; + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(5000); + uint256 maxDebtAmount = 0.86 ether; + + IBorrowControllerV2.SwapParams memory swapParams; + swapParams.data = fromHex( + string.concat( + "83bd37f9000a000b041dcd65000801f447c4595e16b000068d00019b57DcA972Db5D8866c630554AcdbDfE58b2659c00000001", + this.remove0x(Strings.toHexString(address(borrowController))), + "000000010803020801b9269d0f2801010102011dc097350301010103010019011fe3097c0b010104010001240149360b0101050100012fba5f510b0101060100000b0101070100ff0000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583182af49447d8a07e3bd95bd0d56f35241523fbab1db86e7fe4074e3c29d2fd0ed1d104c00e11a196b4ef385d20247f5d0f9cd2e3f716b9a55e71df9347fcdc35463e3770c2fb992716cd070b63540b947f6416e1ed89a3abca179f971b30555eb2234f30c6c9ab1c1dc392b53f9fb2ea6d9dace5f99efdc480000000000000000000000000000000000000000" + ) + ); + swapParams.amount = usdc.amount(500); + swapParams.inSubstitute = address(wausdc); + + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + vm.prank(user); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + maxDebtAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + + LoanPosition memory afterLoanPosition = loanPositionManager.getPosition(positionId); + + assertEq(usdc.balanceOf(user), beforeUSDCBalance + collateralAmount - swapParams.amount, "USDC_BALANCE"); + assertGe(user.balance, beforeETHBalance, "NATIVE_BALANCE"); + assertEq(beforeLoanPosition.expiredWith, afterLoanPosition.expiredWith, "POSITION_EXPIRE_EPOCH"); + assertEq( + beforeLoanPosition.collateralAmount - collateralAmount, + afterLoanPosition.collateralAmount, + "POSITION_COLLATERAL_AMOUNT" + ); + assertLe(afterLoanPosition.debtAmount, maxDebtAmount, "POSITION_DEBT_AMOUNT"); + assertGe(afterLoanPosition.debtAmount, 0.7 ether, "POSITION_DEBT_AMOUNT"); + assertEq(beforeLoanPosition.collateralToken, afterLoanPosition.collateralToken, "POSITION_COLLATERAL_TOKEN"); + assertEq(beforeLoanPosition.debtToken, afterLoanPosition.debtToken, "POSITION_DEBT_TOKEN"); + } + + function testExpiredBorrowMore() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + // loan duration is 2 epochs + vm.warp(EpochLibrary.current().add(2).startTime()); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ILoanPositionManagerTypes.FullRepaymentRequired.selector)); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount, + beforeLoanPosition.debtAmount + 0.5 ether, + type(int256).max, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + } + + function testExpiredReduceCollateral() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(123); + // loan duration is 2 epochs + vm.warp(EpochLibrary.current().add(2).startTime()); + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(ILoanPositionManagerTypes.FullRepaymentRequired.selector)); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + beforeLoanPosition.debtAmount, + 0, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + } + + function testOthersPosition() public { + uint256 positionId = _initialBorrow(user, wausdc, waweth, usdc.amount(10000), 1 ether, 2); + LoanPosition memory beforeLoanPosition = loanPositionManager.getPosition(positionId); + uint256 collateralAmount = usdc.amount(123); + // loan duration is 2 epochs + PermitSignature memory permit721Params = + vm.signPermit(1, loanPositionManager, address(borrowController), positionId); + + IBorrowControllerV2.SwapParams memory swapParams; + vm.expectRevert(abi.encodeWithSelector(IControllerV2.InvalidAccess.selector)); + borrowController.adjust( + positionId, + beforeLoanPosition.collateralAmount - collateralAmount, + beforeLoanPosition.debtAmount, + type(int256).max, + beforeLoanPosition.expiredWith, + swapParams, + permit721Params, + emptyERC20PermitParams, + emptyERC20PermitParams + ); + } + + // Convert an hexadecimal character to their value + function fromHexChar(uint8 c) public pure returns (uint8) { + if (bytes1(c) >= bytes1("0") && bytes1(c) <= bytes1("9")) { + return c - uint8(bytes1("0")); + } + if (bytes1(c) >= bytes1("a") && bytes1(c) <= bytes1("f")) { + return 10 + c - uint8(bytes1("a")); + } + if (bytes1(c) >= bytes1("A") && bytes1(c) <= bytes1("F")) { + return 10 + c - uint8(bytes1("A")); + } + revert("fail"); + } + + // Convert an hexadecimal string to raw bytes + function fromHex(string memory s) public pure returns (bytes memory) { + bytes memory ss = bytes(s); + require(ss.length % 2 == 0); // length must be even + bytes memory r = new bytes(ss.length / 2); + for (uint256 i = 0; i < ss.length / 2; ++i) { + r[i] = bytes1(fromHexChar(uint8(ss[2 * i])) * 16 + fromHexChar(uint8(ss[2 * i + 1]))); + } + return r; + } + + function remove0x(string calldata s) external pure returns (string memory) { + return s[2:]; + } + + function assertEq(Epoch e1, Epoch e2, string memory err) internal { + assertEq(Epoch.unwrap(e1), Epoch.unwrap(e2), err); + } +} diff --git a/test/foundry/integration/CouponLiquidator.t.sol b/test/foundry/integration/CouponLiquidator.t.sol index 5331c68..42cd0fd 100644 --- a/test/foundry/integration/CouponLiquidator.t.sol +++ b/test/foundry/integration/CouponLiquidator.t.sol @@ -222,7 +222,7 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv address recipient = address(this); uint256 beforeUSDCBalance = usdc.balanceOf(recipient); - uint256 beforeWETHBalance = weth.balanceOf(recipient); + uint256 beforeNativeBalance = recipient.balance; address[] memory assets = new address[](3); assets[0] = Constants.COUPON_USDC_SUBSTITUTE; @@ -250,7 +250,7 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv couponLiquidator.liquidate(positionId, usdc.amount(500), data, 0, recipient); assertEq(usdc.balanceOf(recipient) - beforeUSDCBalance, 3344321, "USDC_BALANCE"); - assertEq(weth.balanceOf(recipient) - beforeWETHBalance, 3348150879705280, "WETH_BALANCE"); + assertEq(recipient.balance - beforeNativeBalance, 3348150879705280, "ETH_BALANCE"); } function testLiquidatorWithRouterAndOwnLiquidity() public { @@ -433,4 +433,6 @@ contract CouponLiquidatorIntegrationTest is Test, CloberMarketSwapCallbackReceiv function assertEq(Epoch e1, Epoch e2, string memory err) internal { assertEq(Epoch.unwrap(e1), Epoch.unwrap(e2), err); } + + receive() external payable {} } diff --git a/test/foundry/integration/DepositControllerV2.t.sol b/test/foundry/integration/DepositControllerV2.t.sol new file mode 100644 index 0000000..dd60373 --- /dev/null +++ b/test/foundry/integration/DepositControllerV2.t.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {Constants} from "../Constants.sol"; +import {ForkUtils, ERC20Utils, Utils, PermitSignLibrary} from "../Utils.sol"; +import {IAssetPool} from "../../../contracts/interfaces/IAssetPool.sol"; +import {ICouponManager} from "../../../contracts/interfaces/ICouponManager.sol"; +import {IControllerV2} from "../../../contracts/interfaces/IControllerV2.sol"; +import {IAaveTokenSubstitute} from "../../../contracts/interfaces/IAaveTokenSubstitute.sol"; +import {IERC721Permit} from "../../../contracts/interfaces/IERC721Permit.sol"; +import {IBondPositionManager} from "../../../contracts/interfaces/IBondPositionManager.sol"; +import {Coupon, CouponLibrary} from "../../../contracts/libraries/Coupon.sol"; +import {CouponKey, CouponKeyLibrary} from "../../../contracts/libraries/CouponKey.sol"; +import {Epoch, EpochLibrary} from "../../../contracts/libraries/Epoch.sol"; +import {BondPosition} from "../../../contracts/libraries/BondPosition.sol"; +import {Wrapped1155MetadataBuilder} from "../../../contracts/libraries/Wrapped1155MetadataBuilder.sol"; +import {ERC20PermitParams, PermitSignature} from "../../../contracts/libraries/PermitParams.sol"; +import {IWrapped1155Factory} from "../../../contracts/external/wrapped1155/IWrapped1155Factory.sol"; +import {Tick} from "../../../contracts/external/clober-v2/Tick.sol"; +import {IController} from "../../../contracts/external/clober-v2/IController.sol"; +import {BookIdLibrary} from "../../../contracts/external/clober-v2/BookId.sol"; +import {IBookManager} from "../../../contracts/external/clober-v2/IBookManager.sol"; +import {IHooks} from "../../../contracts/external/clober-v2/IHooks.sol"; +import {Currency, CurrencyLibrary} from "../../../contracts/external/clober-v2/Currency.sol"; +import {FeePolicy, FeePolicyLibrary} from "../../../contracts/external/clober-v2/FeePolicy.sol"; +import {DepositControllerV2} from "../../../contracts/DepositControllerV2.sol"; +import {AaveTokenSubstitute} from "../../../contracts/AaveTokenSubstitute.sol"; +import {MockBookManager} from "../mocks/MockBookManager.sol"; +import {MockController} from "../mocks/MockController.sol"; + +contract DepositControllerV2IntegrationTest is Test, ERC1155Holder { + using FeePolicyLibrary for FeePolicy; + using Strings for *; + using ERC20Utils for IERC20; + using CouponKeyLibrary for CouponKey; + using EpochLibrary for Epoch; + using PermitSignLibrary for Vm; + using BookIdLibrary for IBookManager.BookKey; + + address public constant MARKET_MAKER = address(999123); + + IAssetPool public assetPool; + DepositControllerV2 public depositController; + IBondPositionManager public bondPositionManager; + IWrapped1155Factory public wrapped1155Factory; + ICouponManager public couponManager; + IController public cloberController; + IBookManager public bookManager; + IERC20 public usdc; + address public wausdc; + address public waweth; + address public user; + ERC20PermitParams public emptyERC20PermitParams; + PermitSignature public emptyERC721PermitParams; + + CouponKey[] public couponKeys; + address[] public wrappedCoupons; + + function setUp() public { + ForkUtils.fork(vm, 192219712); + user = vm.addr(1); + + usdc = IERC20(Constants.USDC); + vm.startPrank(Constants.USDC_WHALE); + usdc.transfer(user, usdc.amount(1_000_000)); + usdc.transfer(address(this), usdc.amount(1_000_000)); + vm.stopPrank(); + vm.deal(user, 1_000_000 ether); + vm.deal(address(this), 1_000_000 ether); + (bool success,) = payable(Constants.WETH).call{value: 500_000 ether}(""); + require(success, "transfer failed"); + + wrapped1155Factory = IWrapped1155Factory(Constants.WRAPPED1155_FACTORY); + wausdc = Constants.COUPON_USDC_SUBSTITUTE; + waweth = Constants.COUPON_WETH_SUBSTITUTE; + assetPool = IAssetPool(Constants.COUPON_ASSET_POOL); + couponManager = ICouponManager(Constants.COUPON_COUPON_MANAGER); + bondPositionManager = IBondPositionManager(Constants.COUPON_BOND_POSITION_MANAGER); + bookManager = IBookManager(Constants.CLOBER_BOOK_MANAGER); + cloberController = IController(Constants.CLOBER_CONTROLLER); + + depositController = new DepositControllerV2( + Constants.WRAPPED1155_FACTORY, + address(cloberController), + address(bookManager), + address(couponManager), + Constants.WETH, + address(bondPositionManager) + ); + + usdc.approve(wausdc, usdc.amount(3_000)); + IAaveTokenSubstitute(wausdc).mint(usdc.amount(3_000), address(this)); + IERC20(Constants.WETH).approve(waweth, 3_000 ether); + IAaveTokenSubstitute(waweth).mint(3_000 ether, address(this)); + + // create wrapped1155 + couponKeys.push(CouponKey({asset: wausdc, epoch: EpochLibrary.current()})); + couponKeys.push(CouponKey({asset: waweth, epoch: EpochLibrary.current()})); + couponKeys.push(CouponKey({asset: wausdc, epoch: EpochLibrary.current().add(1)})); + couponKeys.push(CouponKey({asset: waweth, epoch: EpochLibrary.current().add(1)})); + + IController.MakeOrderParams[] memory makeOrderParamsList = new IController.MakeOrderParams[](8); + address[] memory tokensToSettle = new address[](6); + for (uint256 i = 0; i < 4; i++) { + CouponKey memory key = couponKeys[i]; + IController.OpenBookParams[] memory openBookParamsList = new IController.OpenBookParams[](2); + address wrappedToken = wrapped1155Factory.requireWrapped1155( + address(couponManager), key.toId(), Wrapped1155MetadataBuilder.buildWrapped1155Metadata(couponKeys[i]) + ); + wrappedCoupons.push(wrappedToken); + IHooks hooks; + IBookManager.BookKey memory sellBookKey = IBookManager.BookKey({ + base: Currency.wrap(wrappedToken), + unit: i % 2 == 0 ? 1 : 10 ** 6, + quote: Currency.wrap(i % 2 == 0 ? wausdc : waweth), + makerPolicy: FeePolicyLibrary.encode(true, 100), + hooks: hooks, + takerPolicy: FeePolicyLibrary.encode(true, 100) + }); + IBookManager.BookKey memory buyBookKey = IBookManager.BookKey({ + base: Currency.wrap(i % 2 == 0 ? wausdc : waweth), + unit: i % 2 == 0 ? 1 : 10 ** 6, + quote: Currency.wrap(wrappedCoupons[i]), + makerPolicy: FeePolicyLibrary.encode(true, 100), + hooks: hooks, + takerPolicy: FeePolicyLibrary.encode(true, 100) + }); + + openBookParamsList[0] = IController.OpenBookParams({key: sellBookKey, hookData: ""}); + openBookParamsList[1] = IController.OpenBookParams({key: buyBookKey, hookData: ""}); + cloberController.open(openBookParamsList, uint64(block.timestamp)); + depositController.setCouponBookKey(key, sellBookKey, buyBookKey); + + uint256 amount = IERC20(wrappedToken).amount(600); + Coupon[] memory coupons = Utils.toArr(Coupon(key, amount)); + vm.prank(Constants.COUPON_LOAN_POSITION_MANAGER); + couponManager.mintBatch(address(this), coupons, ""); + couponManager.safeBatchTransferFrom( + address(this), + address(wrapped1155Factory), + coupons, + Wrapped1155MetadataBuilder.buildWrapped1155Metadata(couponKeys[i]) + ); + + makeOrderParamsList[i * 2] = IController.MakeOrderParams({ + id: sellBookKey.toId(), + tick: Tick.wrap(-39122), + quoteAmount: i % 2 == 0 ? IERC20(wausdc).amount(500) : IERC20(waweth).amount(500), + hookData: "" + }); + makeOrderParamsList[i * 2 + 1] = IController.MakeOrderParams({ + id: buyBookKey.toId(), + tick: Tick.wrap(39122), + quoteAmount: IERC20(wrappedToken).amount(500), + hookData: "" + }); + tokensToSettle[i] = wrappedToken; + IERC20(wrappedToken).approve(address(cloberController), type(uint256).max); + } + + IERC20(wausdc).approve(address(cloberController), type(uint256).max); + IERC20(waweth).approve(address(cloberController), type(uint256).max); + tokensToSettle[4] = address(wausdc); + tokensToSettle[5] = address(waweth); + IController.ERC20PermitParams[] memory permitParamsList; + cloberController.make(makeOrderParamsList, tokensToSettle, permitParamsList, uint64(block.timestamp)); + } + + function _checkWrappedTokenAlmost0Balance(address who) internal { + for (uint256 i = 0; i < wrappedCoupons.length; ++i) { + assertLt( + IERC20(wrappedCoupons[i]).balanceOf(who), + i % 2 == 0 ? 100 : 1e14, + string.concat(who.toHexString(), " WRAPPED_TOKEN_", i.toString()) + ); + } + } + + function testDeposit() public { + vm.startPrank(user); + uint256 amount = usdc.amount(10); + + uint256 beforeBalance = usdc.balanceOf(user); + + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit( + wausdc, + amount, + EpochLibrary.current(), + 0, + vm.signPermit(1, IERC20Permit(Constants.USDC), address(depositController), amount - 100) + ); + + BondPosition memory position = bondPositionManager.getPosition(tokenId); + + assertEq(bondPositionManager.ownerOf(tokenId), user, "POSITION_OWNER"); + assertGt(usdc.balanceOf(user), beforeBalance - amount, "USDC_BALANCE"); + console.log("diff", usdc.balanceOf(user) - (beforeBalance - amount)); + assertEq(position.asset, wausdc, "POSITION_ASSET"); + assertEq(position.amount, amount, "POSITION_AMOUNT"); + assertEq(position.expiredWith, EpochLibrary.current(), "POSITION_EXPIRED_WITH"); + assertEq(position.nonce, 0, "POSITION_NONCE"); + _checkWrappedTokenAlmost0Balance(address(depositController)); + + vm.stopPrank(); + } + + function testDepositOverSlippage() public { + uint256 amount = usdc.amount(10); + + ERC20PermitParams memory permitParams = + vm.signPermit(1, IERC20Permit(Constants.USDC), address(depositController), amount); + vm.expectRevert(abi.encodeWithSelector(IControllerV2.ControllerSlippage.selector)); + vm.prank(user); + depositController.deposit(wausdc, amount, EpochLibrary.current(), int256(amount * 4 / 100), permitParams); + } + + function testDepositNative() public { + vm.startPrank(user); + uint256 amount = 10 ether; + + uint256 beforeBalance = user.balance; + + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit{value: amount}(waweth, amount, EpochLibrary.current(), 0, emptyERC20PermitParams); + + BondPosition memory position = bondPositionManager.getPosition(tokenId); + + assertEq(bondPositionManager.ownerOf(tokenId), user, "POSITION_OWNER"); + assertGt(user.balance, beforeBalance - amount, "NATIVE_BALANCE"); + console.log("diff", user.balance - (beforeBalance - amount)); + assertEq(position.asset, waweth, "POSITION_ASSET"); + assertEq(position.amount, amount, "POSITION_AMOUNT"); + assertEq(position.expiredWith, EpochLibrary.current(), "POSITION_EXPIRED_WITH"); + assertEq(position.nonce, 0, "POSITION_NONCE"); + _checkWrappedTokenAlmost0Balance(address(depositController)); + + vm.stopPrank(); + } + + function testWithdraw() public { + vm.startPrank(user); + uint256 amount = usdc.amount(10); + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit( + wausdc, + amount, + EpochLibrary.current(), + 0, + vm.signPermit(1, IERC20Permit(Constants.USDC), address(depositController), amount) + ); + + BondPosition memory beforePosition = bondPositionManager.getPosition(tokenId); + uint256 beforeBalance = usdc.balanceOf(user); + + depositController.adjust( + tokenId, + amount / 2, + beforePosition.expiredWith, + type(int256).max, + emptyERC20PermitParams, + vm.signPermit(1, bondPositionManager, address(depositController), tokenId) + ); + + BondPosition memory afterPosition = bondPositionManager.getPosition(tokenId); + + assertEq(bondPositionManager.ownerOf(tokenId), user, "POSITION_OWNER_0"); + assertLt(usdc.balanceOf(user), beforeBalance + amount / 2, "USDC_BALANCE_0"); + console.log("diff", beforeBalance + amount / 2 - usdc.balanceOf(user)); + assertEq(afterPosition.asset, wausdc, "POSITION_ASSET_0"); + assertEq(afterPosition.amount, beforePosition.amount - amount / 2, "POSITION_AMOUNT_0"); + assertEq(afterPosition.expiredWith, beforePosition.expiredWith, "POSITION_EXPIRED_WITH_0"); + assertEq(afterPosition.nonce, beforePosition.nonce + 1, "POSITION_NONCE_0"); + + beforeBalance = usdc.balanceOf(user); + beforePosition = afterPosition; + + depositController.adjust( + tokenId, 0, beforePosition.expiredWith, type(int256).max, emptyERC20PermitParams, emptyERC721PermitParams + ); + + afterPosition = bondPositionManager.getPosition(tokenId); + + vm.expectRevert("ERC721: invalid token ID"); + bondPositionManager.ownerOf(tokenId); + assertLt(usdc.balanceOf(user), beforeBalance + beforePosition.amount, "USDC_BALANCE_1"); + console.log("diff", beforeBalance + beforePosition.amount - usdc.balanceOf(user)); + assertEq(afterPosition.asset, wausdc, "POSITION_ASSET_1"); + assertEq(afterPosition.amount, 0, "POSITION_AMOUNT_1"); + assertEq(afterPosition.expiredWith, EpochLibrary.lastExpiredEpoch(), "POSITION_EXPIRED_WITH_1"); + assertEq(afterPosition.nonce, beforePosition.nonce, "POSITION_NONCE_1"); + _checkWrappedTokenAlmost0Balance(address(depositController)); + + vm.stopPrank(); + } + + function testWithdrawMaxMinusOne() public { + vm.startPrank(user); + uint256 amount = usdc.amount(10); + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit( + wausdc, + amount, + EpochLibrary.current(), + 0, + vm.signPermit(1, IERC20Permit(Constants.USDC), address(depositController), amount) + ); + + BondPosition memory beforePosition = bondPositionManager.getPosition(tokenId); + uint256 beforeBalance = usdc.balanceOf(user); + + depositController.adjust( + tokenId, + 1, + beforePosition.expiredWith, + type(int256).max, + emptyERC20PermitParams, + vm.signPermit(1, bondPositionManager, address(depositController), tokenId) + ); + + BondPosition memory afterPosition = bondPositionManager.getPosition(tokenId); + + assertEq(bondPositionManager.ownerOf(tokenId), user, "POSITION_OWNER_0"); + assertLt(usdc.balanceOf(user), beforeBalance + amount, "USDC_BALANCE_0"); + assertEq(afterPosition.asset, wausdc, "POSITION_ASSET_0"); + assertEq(afterPosition.amount, 1, "POSITION_AMOUNT_0"); + assertEq(afterPosition.expiredWith, beforePosition.expiredWith, "POSITION_EXPIRED_WITH_0"); + assertEq(afterPosition.nonce, beforePosition.nonce + 1, "POSITION_NONCE_0"); + + vm.stopPrank(); + } + + function testWithdrawNative() public { + vm.startPrank(user); + uint256 amount = 10 ether; + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit{value: amount}( + waweth, amount, EpochLibrary.current().add(1), 0, emptyERC20PermitParams + ); + + BondPosition memory beforePosition = bondPositionManager.getPosition(tokenId); + uint256 beforeBalance = user.balance; + + depositController.adjust( + tokenId, + amount / 2, + beforePosition.expiredWith, + type(int256).max, + emptyERC20PermitParams, + vm.signPermit(1, bondPositionManager, address(depositController), tokenId) + ); + + BondPosition memory afterPosition = bondPositionManager.getPosition(tokenId); + + assertEq(bondPositionManager.ownerOf(tokenId), user, "POSITION_OWNER"); + assertLt(user.balance, beforeBalance + amount / 2, "NATIVE_BALANCE"); + console.log("diff", beforeBalance + amount / 2 - user.balance); + assertEq(afterPosition.asset, waweth, "POSITION_ASSET"); + assertEq(afterPosition.amount, beforePosition.amount - amount / 2, "POSITION_AMOUNT"); + assertEq(afterPosition.expiredWith, beforePosition.expiredWith, "POSITION_EXPIRED_WITH"); + assertEq(afterPosition.nonce, beforePosition.nonce + 1, "POSITION_NONCE"); + + beforeBalance = user.balance; + beforePosition = afterPosition; + + depositController.adjust( + tokenId, 0, beforePosition.expiredWith, type(int256).max, emptyERC20PermitParams, emptyERC721PermitParams + ); + + afterPosition = bondPositionManager.getPosition(tokenId); + + vm.expectRevert("ERC721: invalid token ID"); + bondPositionManager.ownerOf(tokenId); + assertLt(user.balance, beforeBalance + beforePosition.amount, "NATIVE_BALANCE_1"); + console.log("diff", beforeBalance + beforePosition.amount - user.balance); + assertEq(afterPosition.asset, waweth, "POSITION_ASSET_1"); + assertEq(afterPosition.amount, 0, "POSITION_AMOUNT_1"); + assertEq(afterPosition.expiredWith, EpochLibrary.lastExpiredEpoch(), "POSITION_EXPIRED_WITH_1"); + assertEq(afterPosition.nonce, beforePosition.nonce, "POSITION_NONCE_1"); + _checkWrappedTokenAlmost0Balance(address(depositController)); + + vm.stopPrank(); + } + + function testCollect() public { + vm.startPrank(user); + uint256 amount = usdc.amount(10); + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit( + wausdc, + amount, + EpochLibrary.current(), + 0, + vm.signPermit(1, IERC20Permit(Constants.USDC), address(depositController), amount) + ); + vm.warp(EpochLibrary.current().add(1).startTime()); + + BondPosition memory beforePosition = bondPositionManager.getPosition(tokenId); + uint256 beforeBalance = usdc.balanceOf(user); + + depositController.adjust( + tokenId, + 0, + beforePosition.expiredWith, + 0, + emptyERC20PermitParams, + vm.signPermit(1, bondPositionManager, address(depositController), tokenId) + ); + + BondPosition memory afterPosition = bondPositionManager.getPosition(tokenId); + + vm.expectRevert("ERC721: invalid token ID"); + bondPositionManager.ownerOf(tokenId); + assertEq(usdc.balanceOf(user), beforeBalance + beforePosition.amount, "USDC_BALANCE"); + assertEq(afterPosition.asset, wausdc, "POSITION_ASSET"); + assertEq(afterPosition.amount, 0, "POSITION_AMOUNT"); + assertEq(afterPosition.expiredWith, EpochLibrary.lastExpiredEpoch(), "POSITION_EXPIRED_WITH"); + assertEq(afterPosition.nonce, beforePosition.nonce + 1, "POSITION_NONCE"); + + vm.stopPrank(); + } + + function testCollectNative() public { + vm.startPrank(user); + uint256 amount = 10 ether; + uint256 tokenId = bondPositionManager.nextId(); + depositController.deposit{value: amount}(waweth, amount, EpochLibrary.current(), 0, emptyERC20PermitParams); + vm.warp(EpochLibrary.current().add(1).startTime()); + + BondPosition memory beforePosition = bondPositionManager.getPosition(tokenId); + uint256 beforeBalance = user.balance; + + depositController.adjust( + tokenId, + 0, + beforePosition.expiredWith, + 0, + emptyERC20PermitParams, + vm.signPermit(1, bondPositionManager, address(depositController), tokenId) + ); + + BondPosition memory afterPosition = bondPositionManager.getPosition(tokenId); + + vm.expectRevert("ERC721: invalid token ID"); + bondPositionManager.ownerOf(tokenId); + assertEq(user.balance, beforeBalance + beforePosition.amount, "NATIVE_BALANCE"); + assertEq(afterPosition.asset, waweth, "POSITION_ASSET"); + assertEq(afterPosition.amount, 0, "POSITION_AMOUNT"); + assertEq(afterPosition.expiredWith, EpochLibrary.lastExpiredEpoch(), "POSITION_EXPIRED_WITH"); + assertEq(afterPosition.nonce, beforePosition.nonce + 1, "POSITION_NONCE"); + + vm.stopPrank(); + } + + function assertEq(Epoch e1, Epoch e2, string memory err) internal { + assertEq(Epoch.unwrap(e1), Epoch.unwrap(e2), err); + } +} diff --git a/test/foundry/mocks/MockBookManager.sol b/test/foundry/mocks/MockBookManager.sol new file mode 100644 index 0000000..74b8d09 --- /dev/null +++ b/test/foundry/mocks/MockBookManager.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../../../contracts/external/clober-v2/BookId.sol"; +import "../../../contracts/external/clober-v2/IHooks.sol"; +import "../../../contracts/external/clober-v2/Currency.sol"; +import "../../../contracts/external/clober-v2/FeePolicy.sol"; + +contract MockBookManager { + function getBookKey(BookId) external pure returns (IBookManager.BookKey memory) { + IHooks hooks; + return IBookManager.BookKey({ + base: CurrencyLibrary.NATIVE, + unit: 10 ** 6, + quote: CurrencyLibrary.NATIVE, + makerPolicy: FeePolicyLibrary.encode(true, -100), + hooks: hooks, + takerPolicy: FeePolicyLibrary.encode(true, -100) + }); + } +} diff --git a/test/foundry/mocks/MockController.sol b/test/foundry/mocks/MockController.sol new file mode 100644 index 0000000..2986a05 --- /dev/null +++ b/test/foundry/mocks/MockController.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "../../../contracts/external/clober-v2/IController.sol"; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Constants} from "../Constants.sol"; + +contract MockController { + using SafeERC20 for IERC20; + + address[] public wrappedCoupons; + + constructor(address[] memory _wrappedCoupons) { + wrappedCoupons = _wrappedCoupons; + } + + function execute( + IController.Action[] calldata actionList, + bytes[] calldata paramsDataList, + address[] calldata, + IController.ERC20PermitParams[] calldata, + IController.ERC721PermitParams[] calldata, + uint64 + ) external payable returns (OrderId[] memory ids) { + ids = new OrderId[](0); + uint256 length = actionList.length; + for (uint256 i = 0; i < length; ++i) { + if (actionList[i] == IController.Action.SPEND) { + IController.SpendOrderParams memory params = + abi.decode(paramsDataList[i], (IController.SpendOrderParams)); + if (BookId.unwrap(params.id) == 5123218587801245791363875878418863513309367190045666848596) { + IERC20(wrappedCoupons[0]).safeTransferFrom(msg.sender, address(this), params.baseAmount - 1); + IERC20(Constants.COUPON_USDC_SUBSTITUTE).safeTransfer(msg.sender, params.baseAmount / 50); + } else if (BookId.unwrap(params.id) == 2050871663329071981865486441148209933927850794997083594453) { + IERC20(wrappedCoupons[1]).safeTransferFrom(msg.sender, address(this), params.baseAmount - 1); + IERC20(Constants.COUPON_WETH_SUBSTITUTE).safeTransfer(msg.sender, params.baseAmount / 50); + } else if (BookId.unwrap(params.id) == 660651708250344502034174152287312671762346957581964646473) { + IERC20(wrappedCoupons[3]).safeTransferFrom(msg.sender, address(this), params.baseAmount - 1); + IERC20(Constants.COUPON_WETH_SUBSTITUTE).safeTransfer(msg.sender, params.baseAmount / 50); + } + } else if (actionList[i] == IController.Action.TAKE) { + IController.TakeOrderParams memory params = abi.decode(paramsDataList[i], (IController.TakeOrderParams)); + if (BookId.unwrap(params.id) == 3982373688268902797607790123826322480679588654359190790390) { + IERC20(Constants.COUPON_WETH_SUBSTITUTE).safeTransferFrom( + msg.sender, address(this), params.quoteAmount / 50 + ); + IERC20(wrappedCoupons[1]).safeTransfer(msg.sender, params.quoteAmount + 1); + } else if (BookId.unwrap(params.id) == 5028964825499590552846748960829820796991540535456089637028) { + IERC20(Constants.COUPON_USDC_SUBSTITUTE).safeTransferFrom( + msg.sender, address(this), params.quoteAmount / 50 + ); + IERC20(wrappedCoupons[0]).safeTransfer(msg.sender, params.quoteAmount + 1); + } else if (BookId.unwrap(params.id) == 4240473993678446490798627048287957591242783559797607980606) { + IERC20(Constants.COUPON_WETH_SUBSTITUTE).safeTransferFrom( + msg.sender, address(this), params.quoteAmount / 50 + ); + IERC20(wrappedCoupons[3]).safeTransfer(msg.sender, params.quoteAmount + 1); + } + } + } + } + + receive() external payable {} +} diff --git a/test/foundry/unit/libraries/Substitute.t.sol b/test/foundry/unit/libraries/Substitute.t.sol index 91bd605..dab6040 100644 --- a/test/foundry/unit/libraries/Substitute.t.sol +++ b/test/foundry/unit/libraries/Substitute.t.sol @@ -25,29 +25,29 @@ contract SubstituteLibraryUnitTest is Test { IERC20(weth).approve(address(waweth), type(uint256).max); } - function testEnsureBalance() public { - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 0 ether}), - Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 0, wrapperUnderlying: 0}) + function testMintAll() public { + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 0 ether}), + Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 1 ether, wrapperUnderlying: -1 ether}) ); - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 0.5 ether}), - Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 0, wrapperUnderlying: 0}) + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 0.5 ether}), + Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 1 ether, wrapperUnderlying: -1 ether}) ); - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 1 ether}), - Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 0, wrapperUnderlying: 0}) + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 1 ether}), + Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 1 ether, wrapperUnderlying: -1 ether}) ); - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 1.5 ether}), - Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 0.5 ether, wrapperUnderlying: -0.5 ether}) + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 1.5 ether}), + Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 1 ether, wrapperUnderlying: -1 ether}) ); - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 2 ether}), + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 2 ether}), Expected({thisSubstitute: 0, thisUnderlying: 0, wrapperSubstitute: 1 ether, wrapperUnderlying: -1 ether}) ); - _testEnsureThisBalance( - Input({substitute: 1 ether, underlying: 1 ether, ensure: 2.3 ether}), + _testMintAll( + Input({substitute: 1 ether, underlying: 1 ether, minRequired: 2.3 ether}), Expected({ thisSubstitute: 0, thisUnderlying: -0.3 ether, @@ -60,7 +60,7 @@ contract SubstituteLibraryUnitTest is Test { struct Input { uint256 substitute; uint256 underlying; - uint256 ensure; + uint256 minRequired; } struct Expected { @@ -70,7 +70,7 @@ contract SubstituteLibraryUnitTest is Test { int256 wrapperUnderlying; } - function _testEnsureThisBalance(Input memory input, Expected memory expected) internal { + function _testMintAll(Input memory input, Expected memory expected) internal { SubstituteLibraryWrapper wrapper = new SubstituteLibraryWrapper(); IERC20(weth).approve(address(wrapper), type(uint256).max); @@ -82,7 +82,7 @@ contract SubstituteLibraryUnitTest is Test { int256 beforeWrapperSubstitute = int256(IERC20(waweth).balanceOf(address(wrapper))); int256 beforeWrapperUnderlying = int256(IERC20(weth).balanceOf(address(wrapper))); - wrapper.ensureThisBalance(waweth, address(this), input.ensure); + wrapper.mintAll(waweth, address(this), input.minRequired); int256 afterThisSubstitute = int256(IERC20(waweth).balanceOf(address(this))); int256 afterThisUnderlying = int256(IERC20(weth).balanceOf(address(this))); @@ -97,7 +97,7 @@ contract SubstituteLibraryUnitTest is Test { } contract SubstituteLibraryWrapper { - function ensureThisBalance(address substitute, address payer, uint256 amount) external { - SubstituteLibrary.ensureBalance(ISubstitute(substitute), payer, amount); + function mintAll(address substitute, address payer, uint256 minRequiredBalance) external { + SubstituteLibrary.mintAll(ISubstitute(substitute), payer, minRequiredBalance); } }