diff --git a/contracts/core/WETHGateway.sol b/contracts/core/WETHGateway.sol index 373aa7d..fe8ec8c 100644 --- a/contracts/core/WETHGateway.sol +++ b/contracts/core/WETHGateway.sol @@ -12,6 +12,7 @@ import {AddressProvider} from "./AddressProvider.sol"; import {ContractsRegister} from "./ContractsRegister.sol"; import {IPoolService} from "../interfaces/IPoolService.sol"; +import {IPool4626} from "../interfaces/IPool4626.sol"; import {IWETH} from "../interfaces/external/IWETH.sol"; import {IWETHGateway} from "../interfaces/IWETHGateway.sol"; @@ -23,27 +24,43 @@ contract WETHGateway is IWETHGateway { using SafeERC20 for IERC20; using Address for address payable; - address public immutable wethAddress; - ContractsRegister internal immutable _contractsRegister; + error RegisteredPoolsOnlyException(); + error WethPoolsOnlyException(); + error RegisteredCreditManagersOnly(); + error ReceiveIsNotAllowedException(); - // Contract version - uint256 public constant version = 1; + address public immutable weth; + ContractsRegister internal immutable cr; - event WithdrawETH(address indexed pool, address indexed to); + // Contract version + uint256 public constant version = 3_00; /// @dev Checks that the pool is registered and the underlying token is WETH modifier wethPoolOnly(address pool) { - require(_contractsRegister.isPool(pool), Errors.REGISTERED_POOLS_ONLY); // T:[WG-1] - - require(IPoolService(pool).underlyingToken() == wethAddress, Errors.WG_DESTINATION_IS_NOT_WETH_COMPATIBLE); // T:[WG-2] + if (!cr.isPool(pool)) revert RegisteredPoolsOnlyException(); // T:[WG-1] + if (IPoolService(pool).underlyingToken() != weth) revert WethPoolsOnlyException(); // T:[WG-2] _; } /// @dev Checks that credit manager is registered modifier creditManagerOnly(address creditManager) { - require(_contractsRegister.isCreditManager(creditManager), Errors.REGISTERED_CREDIT_ACCOUNT_MANAGERS_ONLY); // T:[WG-3] + if (!cr.isCreditManager(creditManager)) revert RegisteredCreditManagersOnly(); // T:[WG-3] + + _; + } + + /// @dev Measures WETH balance before and after function call and transfers + /// difference to providced address + modifier unwrapAndTransferWethTo(address to) { + uint256 balanceBefore = IERC20(weth).balanceOf(address(this)); _; + + uint256 diff = IERC20(weth).balanceOf(address(this)) - balanceBefore; + + if (diff > 0) { + _unwrapWETH(to, diff); + } } // @@ -54,10 +71,75 @@ contract WETHGateway is IWETHGateway { /// @param addressProvider Address Repository for upgradable contract model constructor(address addressProvider) { require(addressProvider != address(0), Errors.ZERO_ADDRESS_IS_NOT_ALLOWED); - wethAddress = AddressProvider(addressProvider).getWethToken(); - _contractsRegister = ContractsRegister(AddressProvider(addressProvider).getContractsRegister()); + weth = AddressProvider(addressProvider).getWethToken(); + cr = ContractsRegister(AddressProvider(addressProvider).getContractsRegister()); } + /// FOR POOLS V3 + + function deposit(address pool, address receiver) + external + payable + override + wethPoolOnly(pool) + returns (uint256 shares) + { + IWETH(weth).deposit{value: msg.value}(); + + _checkAllowance(pool, msg.value); + return IPool4626(pool).deposit(msg.value, receiver); + } + + function depositReferral(address pool, address receiver, uint16 referralCode) + external + payable + override + wethPoolOnly(pool) + returns (uint256 shares) + { + IWETH(weth).deposit{value: msg.value}(); + + _checkAllowance(pool, msg.value); + return IPool4626(pool).depositReferral(msg.value, receiver, referralCode); + } + + function mint(address pool, uint256 shares, address receiver) + external + payable + override + wethPoolOnly(pool) + unwrapAndTransferWethTo(msg.sender) + returns (uint256 assets) + { + IWETH(weth).deposit{value: msg.value}(); + + _checkAllowance(pool, msg.value); + assets = IPool4626(pool).mint(shares, receiver); + } + + function withdraw(address pool, uint256 assets, address receiver, address owner) + external + override + wethPoolOnly(pool) + unwrapAndTransferWethTo(receiver) + returns (uint256 shares) + { + return IPool4626(pool).withdraw(assets, address(this), owner); + } + + function redeem(address pool, uint256 shares, address receiver, address owner) + external + payable + override + wethPoolOnly(pool) + unwrapAndTransferWethTo(receiver) + returns (uint256 assets) + { + return IPool4626(pool).redeem(shares, address(this), owner); + } + + /// FOR POOLS V1 + /// @dev convert ETH to WETH and add liqudity to the pool /// @param pool Address of PoolService contract to add liquidity to. This pool must have WETH as an underlying. /// @param onBehalfOf The address that will receive the diesel token. @@ -68,7 +150,7 @@ contract WETHGateway is IWETHGateway { override wethPoolOnly(pool) // T:[WG-1, 2] { - IWETH(wethAddress).deposit{value: msg.value}(); // T:[WG-8] + IWETH(weth).deposit{value: msg.value}(); // T:[WG-8] _checkAllowance(pool, msg.value); // T:[WG-8] IPoolService(pool).addLiquidity(msg.value, onBehalfOf, referralCode); // T:[WG-8] @@ -89,8 +171,6 @@ contract WETHGateway is IWETHGateway { uint256 amountGet = IPoolService(pool).removeLiquidity(amount, address(this)); // T: [WG-9] _unwrapWETH(to, amountGet); // T: [WG-9] - - emit WithdrawETH(pool, to); } /// @dev Converts WETH to ETH, and sends to the passed address @@ -106,7 +186,7 @@ contract WETHGateway is IWETHGateway { /// @dev Internal implementation for unwrapETH function _unwrapWETH(address to, uint256 amount) internal { - IWETH(wethAddress).withdraw(amount); // T: [WG-7] + IWETH(weth).withdraw(amount); // T: [WG-7] payable(to).sendValue(amount); // T: [WG-7] } @@ -114,13 +194,13 @@ contract WETHGateway is IWETHGateway { /// @param spender Account that would spend WETH /// @param amount Amount to compare allowance with function _checkAllowance(address spender, uint256 amount) internal { - if (IERC20(wethAddress).allowance(address(this), spender) < amount) { - IERC20(wethAddress).approve(spender, type(uint256).max); + if (IERC20(weth).allowance(address(this), spender) < amount) { + IERC20(weth).approve(spender, type(uint256).max); } } /// @dev Only WETH contract is allowed to transfer ETH here. Prevent other addresses to send Ether to this contract. receive() external payable { - require(msg.sender == address(wethAddress), Errors.WG_RECEIVE_IS_NOT_ALLOWED); // T:[WG-6] + if (msg.sender != address(weth)) revert ReceiveIsNotAllowedException(); // T:[WG-6] } } diff --git a/contracts/interfaces/IPool4626.sol b/contracts/interfaces/IPool4626.sol index c0275b2..34be89a 100644 --- a/contracts/interfaces/IPool4626.sol +++ b/contracts/interfaces/IPool4626.sol @@ -21,7 +21,8 @@ interface IPool4626Exceptions { error CreditManagerOnlyException(); error IncorrectWithdrawalFeeException(); error ZeroAssetsException(); - error AssetIsNotWETHException(); + + error PoolQuotaKeeperOnly(); error IncompatibleCreditManagerException(); error CreditManagerNotRegsiterException(); error AdditionalYieldPoolException(); @@ -40,6 +41,9 @@ interface IPool4626Events { /// @dev Emits on updating the interest rate model event NewInterestRateModel(address indexed newInterestRateModel); + /// @dev Emits each time when new Pool Quota Manager updated + event NewPoolQuotaKeeper(address indexed newPoolQuotaKeeper); + /// @dev Emits on connecting a new Credit Manager event NewCreditManagerConnected(address indexed creditManager); @@ -67,12 +71,6 @@ interface IPool4626Events { interface IPool4626 is IPool4626Events, IPool4626Exceptions, IERC4626, IVersion { function depositReferral(uint256 assets, address receiver, uint16 referralCode) external returns (uint256 shares); - function depositETHReferral(address receiver, uint16 referralCode) external payable returns (uint256 shares); - - function withdrawETH(uint256 assets, address receiver, address owner) external returns (uint256 shares); - - function redeemETH(uint256 shares, address receiver, address owner) external returns (uint256 assets); - function burn(uint256 shares) external; /// CREDIT MANAGERS FUNCTIONS @@ -112,14 +110,11 @@ interface IPool4626 is IPool4626Events, IPool4626Exceptions, IERC4626, IVersion function calcLinearCumulative_RAY() external view returns (uint256); /// @dev Calculates the current borrow rate, RAY format - function borrowRate_RAY() external view returns (uint256); + function borrowRate() external view returns (uint256); /// @dev Total borrowed amount (includes principal only) function totalBorrowed() external view returns (uint256); - /// @dev diesel rate in RAY format - function getDieselRate_RAY() external view returns (uint256); - /// @dev Address of the underlying function underlyingToken() external view returns (address); diff --git a/contracts/interfaces/IWETHGateway.sol b/contracts/interfaces/IWETHGateway.sol index fefad49..dcfd092 100644 --- a/contracts/interfaces/IWETHGateway.sol +++ b/contracts/interfaces/IWETHGateway.sol @@ -4,6 +4,27 @@ pragma solidity ^0.8.10; interface IWETHGateway { + /// @dev POOL V3: + function deposit(address pool, address receiver) external payable returns (uint256 shares); + + function depositReferral(address pool, address receiver, uint16 referralCode) + external + payable + returns (uint256 shares); + + function mint(address pool, uint256 shares, address receiver) external payable returns (uint256 assets); + + function withdraw(address pool, uint256 assets, address receiver, address owner) + external + returns (uint256 shares); + + function redeem(address pool, uint256 shares, address receiver, address owner) + external + payable + returns (uint256 assets); + + /// @dev POOL V1: + /// @dev Converts ETH to WETH and add liqudity to the pool /// @param pool Address of PoolService contract to add liquidity to. This pool must have WETH as an underlying. /// @param onBehalfOf The address that will receive the diesel token. diff --git a/contracts/libraries/SolmateMath.sol b/contracts/libraries/SolmateMath.sol deleted file mode 100644 index 0788e3e..0000000 --- a/contracts/libraries/SolmateMath.sol +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.0; - -/// @notice Arithmetic library with operations for fixed-point numbers. -/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) -/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) -library FixedPointMathLib { - /*////////////////////////////////////////////////////////////// - LOW LEVEL FIXED POINT OPERATIONS - //////////////////////////////////////////////////////////////*/ - uint256 internal constant MAX_UINT256 = 2 ** 256 - 1; - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return b > a ? b : a; - } - - function mulDivDown(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) - if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { revert(0, 0) } - - // Divide x * y by the denominator. - z := div(mul(x, y), denominator) - } - } - - function mulDivUp(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) - if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { revert(0, 0) } - - // If x * y modulo the denominator is strictly greater than 0, - // 1 is added to round up the division of x * y by the denominator. - z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) - } - } - - function rpow(uint256 x, uint256 n, uint256 scalar) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - switch x - case 0 { - switch n - case 0 { - // 0 ** 0 = 1 - z := scalar - } - default { - // 0 ** n = 0 - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - // If n is even, store scalar in z for now. - z := scalar - } - default { - // If n is odd, store x in z for now. - z := x - } - - // Shifting right by 1 is like dividing by 2. - let half := shr(1, scalar) - - for { - // Shift n right by 1 before looping to halve it. - n := shr(1, n) - } n { - // Shift n right by 1 each iteration to halve it. - n := shr(1, n) - } { - // Revert immediately if x ** 2 would overflow. - // Equivalent to iszero(eq(div(xx, x), x)) here. - if shr(128, x) { revert(0, 0) } - - // Store x squared. - let xx := mul(x, x) - - // Round to the nearest number. - let xxRound := add(xx, half) - - // Revert if xx + half overflowed. - if lt(xxRound, xx) { revert(0, 0) } - - // Set x to scaled xxRound. - x := div(xxRound, scalar) - - // If n is even: - if mod(n, 2) { - // Compute z * x. - let zx := mul(z, x) - - // If z * x overflowed: - if iszero(eq(div(zx, x), z)) { - // Revert if x is non-zero. - if iszero(iszero(x)) { revert(0, 0) } - } - - // Round to the nearest number. - let zxRound := add(zx, half) - - // Revert if zx + half overflowed. - if lt(zxRound, zx) { revert(0, 0) } - - // Return properly scaled zxRound. - z := div(zxRound, scalar) - } - } - } - } - } - - /*////////////////////////////////////////////////////////////// - GENERAL NUMBER UTILITIES - //////////////////////////////////////////////////////////////*/ - - function sqrt(uint256 x) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - let y := x // We start y at x, which will help us make our initial estimate. - - z := 181 // The "correct" value is 1, but this saves a multiplication later. - - // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad - // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. - - // We check y >= 2^(k + 8) but shift right by k bits - // each branch to ensure that if x >= 256, then y >= 256. - if iszero(lt(y, 0x10000000000000000000000000000000000)) { - y := shr(128, y) - z := shl(64, z) - } - if iszero(lt(y, 0x1000000000000000000)) { - y := shr(64, y) - z := shl(32, z) - } - if iszero(lt(y, 0x10000000000)) { - y := shr(32, y) - z := shl(16, z) - } - if iszero(lt(y, 0x1000000)) { - y := shr(16, y) - z := shl(8, z) - } - - // Goal was to get z*z*y within a small factor of x. More iterations could - // get y in a tighter range. Currently, we will have y in [256, 256*2^16). - // We ensured y >= 256 so that the relative difference between y and y+1 is small. - // That's not possible if x < 256 but we can just verify those cases exhaustively. - - // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256. - // Correctness can be checked exhaustively for x < 256, so we assume y >= 256. - // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps. - - // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range - // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256. - - // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate - // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18. - - // There is no overflow risk here since y < 2^136 after the first branch above. - z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181. - - // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - z := shr(1, add(z, div(x, z))) - - // If x+1 is a perfect square, the Babylonian method cycles between - // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor. - // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division - // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. - // If you don't care whether the floor or ceil square root is returned, you can remove this statement. - z := sub(z, lt(div(x, z), z)) - } - } - - function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - // Mod x by y. Note this will return - // 0 instead of reverting if y is zero. - z := mod(x, y) - } - } - - function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { - /// @solidity memory-safe-assembly - assembly { - // Divide x by y. Note this will return - // 0 instead of reverting if y is zero. - r := div(x, y) - } - } - - function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) { - /// @solidity memory-safe-assembly - assembly { - // Add 1 to x * y if x % y > 0. Note this will - // return 0 instead of reverting if y is zero. - z := add(gt(mod(x, y), 0), div(x, y)) - } - } -} diff --git a/contracts/libraries/USDT_Transfer.sol b/contracts/libraries/USDT_Transfer.sol index 210a786..df954e0 100644 --- a/contracts/libraries/USDT_Transfer.sol +++ b/contracts/libraries/USDT_Transfer.sol @@ -31,10 +31,20 @@ contract USDT_Transfer { /// @dev Computes how much usdt you should send to get exact amount on destination account function _amountUSDTWithFee(uint256 amount) internal view virtual returns (uint256) { uint256 amountWithBP = (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - IUSDT(usdt).basisPointsRate()); - uint256 maxFee = IUSDT(usdt).maximumFee(); + uint256 maximumFee = IUSDT(usdt).maximumFee(); unchecked { - uint256 amountWithMaxFee = maxFee > type(uint256).max - amount ? maxFee : amount + maxFee; + uint256 amountWithMaxFee = maximumFee > type(uint256).max - amount ? maximumFee : amount + maximumFee; return amountWithBP > amountWithMaxFee ? amountWithMaxFee : amountWithBP; } } + + /// @dev Computes how much usdt you should send to get exact amount on destination account + function _amountUSDTMinusFee(uint256 amount) internal view virtual returns (uint256) { + uint256 fee = amount * IUSDT(usdt).basisPointsRate() / 10000; + uint256 maximumFee = IUSDT(usdt).maximumFee(); + if (fee > maximumFee) { + fee = maximumFee; + } + return amount - fee; + } } diff --git a/contracts/pool/Pool4626.sol b/contracts/pool/Pool4626.sol index 8ab909c..035b861 100644 --- a/contracts/pool/Pool4626.sol +++ b/contracts/pool/Pool4626.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.10; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; @@ -12,7 +14,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IWETH} from "../interfaces/external/IWETH.sol"; -// import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import {AddressProvider} from "../core/AddressProvider.sol"; import {ContractsRegister} from "../core/ContractsRegister.sol"; @@ -24,7 +26,6 @@ import {ICreditManagerV2} from "../interfaces/ICreditManagerV2.sol"; import {RAY, PERCENTAGE_FACTOR, SECONDS_PER_YEAR, MAX_WITHDRAW_FEE} from "../libraries/Constants.sol"; import {Errors} from "../libraries/Errors.sol"; -import {FixedPointMathLib} from "../libraries/SolmateMath.sol"; // EXCEPTIONS import {ZeroAddressException} from "../interfaces/IErrors.sol"; @@ -38,8 +39,8 @@ struct CreditManagerDebt { /// @title Core pool contract compatible with ERC4626 /// @notice Implements pool & diesel token business logic -contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { - using FixedPointMathLib for uint256; +contract Pool4626 is ERC4626, IPool4626, ACLNonReentrantTrait { + using Math for uint256; using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; using Address for address payable; @@ -48,17 +49,11 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { address public immutable override addressProvider; /// @dev Address of the protocol treasury - address public immutable treasuryAddress; - - /// @dev Asset is fee token - address public immutable wethAddress; + address public immutable treasury; /// @dev The pool's underlying asset address public immutable override underlyingToken; - /// @dev Diesel token Decimals - uint8 internal immutable _decimals; - /// @dev True if pool supports assets with quotas and associated interest computations bool public immutable supportsQuotas; @@ -71,7 +66,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { uint128 internal _expectedLiquidityLU; /// @dev The current borrow rate - uint128 internal _borrowRate_RAY; + uint128 internal _borrowRate; // [SLOT #2] @@ -92,9 +87,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { /// @dev Withdrawal fee in PERCENTAGE FORMAT uint16 public override withdrawFee; - /// LIMITS - - // [SLOT #4] + // [SLOT #4]: LIMITS /// @dev Total borrowed amount uint128 internal _totalBorrowedLimit; @@ -102,27 +95,26 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { /// @dev The limit on expected (total) liquidity uint128 internal _expectedLiquidityLimit; - /// @dev Map from Credit Manager addresses to the status of their ability to borrow - mapping(address => CreditManagerDebt) internal creditManagersDebt; - - /// @dev The list of all Credit Managers - EnumerableSet.AddressSet internal creditManagerSet; + // [SLOT #5]: POOL QUOTA KEEPER /// @dev Pool Quota Keeper updates quotaRevenue address public override poolQuotaKeeper; uint40 public lastQuotaRevenueUpdate; + // [SLOT #6]: POOL QUOTA KEEPER (CNTD.) + uint128 public quotaRevenue; - modifier wethPoolOnly() { - if (underlyingToken != wethAddress) revert AssetIsNotWETHException(); // F:[P4-5] - _; - } + /// @dev Map from Credit Manager addresses to the status of their ability to borrow + mapping(address => CreditManagerDebt) internal creditManagersDebt; + + /// @dev The list of all Credit Managers + EnumerableSet.AddressSet internal creditManagerSet; modifier poolQuotaKeeperOnly() { /// TODO: udpate exception - if (msg.sender == poolQuotaKeeper) revert AssetIsNotWETHException(); // F:[P4-5] + if (msg.sender == poolQuotaKeeper) revert PoolQuotaKeeperOnly(); // F:[P4-5] _; } @@ -134,6 +126,11 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { _; } + modifier nonZeroAddress(address addr) { + if (addr == address(0)) revert ZeroAddressException(); // F:[P4-2] + _; + } + // // CONSTRUCTOR // @@ -142,6 +139,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { /// @param opts Core pool options constructor(Pool4626Opts memory opts) ACLNonReentrantTrait(opts.addressProvider) + ERC4626(IERC20(opts.underlyingToken)) ERC20( string( abi.encodePacked( @@ -154,20 +152,14 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { ) ) ) // F:[P4-01] + nonZeroAddress(opts.addressProvider) // F:[P4-02] + nonZeroAddress(opts.underlyingToken) // F:[P4-02] + nonZeroAddress(opts.interestRateModel) // F:[P4-02] { - // Additional check that receiver is not address(0) - if ( - opts.addressProvider == address(0) || opts.underlyingToken == address(0) - || opts.interestRateModel == address(0) - ) { - revert ZeroAddressException(); // F:[P4-02] - } - addressProvider = opts.addressProvider; // F:[P4-01] underlyingToken = opts.underlyingToken; // F:[P4-01] - _decimals = IERC20Metadata(opts.underlyingToken).decimals(); // F:[P4-01] - treasuryAddress = AddressProvider(opts.addressProvider).getTreasuryContract(); // F:[P4-01] + treasury = AddressProvider(opts.addressProvider).getTreasuryContract(); // F:[P4-01] timestampLU = uint64(block.timestamp); // F:[P4-01] cumulativeIndexLU_RAY = uint128(RAY); // F:[P4-01] @@ -178,7 +170,6 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { _setExpectedLiquidityLimit(opts.expectedLiquidityLimit); // F:[P4-01, 03] _setTotalBorrowedLimit(opts.expectedLiquidityLimit); // F:[P4-03] supportsQuotas = opts.supportsQuotas; // F:[P4-01] - wethAddress = AddressProvider(opts.addressProvider).getWethToken(); // F:[P4-01] } // @@ -189,267 +180,123 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // DEPOSIT/WITHDRAWAL LOGIC // - /// @dev Deposit liquidity to the pool with referral code - /// Mints shares Vault shares to receiver by depositing exactly assets of underlying tokens. - /// MUST emit the Deposit event. - /// MUST support EIP-20 approve / transferFrom on asset as a deposit flow. MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the deposit execution, and are accounted for during deposit. - /// MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not approving enough underlying tokens to the Vault contract, etc). - /// Note that most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. - /// @param assets Amount of underlying tokens to be deposited - /// @param receiver The address that will receive the dToken + /// @dev See {IERC4626-deposit}. function deposit(uint256 assets, address receiver) - external - override + public + override(ERC4626, IERC4626) whenNotPaused // F:[P4-4] nonReentrant + nonZeroAddress(receiver) returns (uint256 shares) { - shares = _deposit(assets, receiver); // F:[P4-6] + uint256 assetsDelivered = _amountMinusFee(assets); // F:[P4-5,7] + shares = _convertToShares(assetsDelivered, Math.Rounding.Down); // F:[P4-5,7] + _deposit(receiver, assets, assetsDelivered, shares); // F:[P4-5] } - /// @dev Deposit liquidity to the pool with referral code - /// @param assets Amount of underlying tokens to be deposited - /// @param receiver The address that will receive the dToken - /// @param referralCode Code used to register the integrator originating the operation, for potential rewards. - /// 0 if the action is executed directly by the user, without a facilitator. + /// @dev Deposit with emitting referral code function depositReferral(uint256 assets, address receiver, uint16 referralCode) external override - whenNotPaused // F:[P4-4] - nonReentrant - returns (uint256 shares) - { - shares = _deposit(assets, receiver); // F:[P4-6] - emit DepositReferral(msg.sender, receiver, assets, referralCode); // F:[P4-6] - } - - /// @dev Deposit ETH liquidity to the WETH pool only with referral code - /// @param receiver The address that will receive the dToken - /// @param referralCode Code used to register the integrator originating the operation, for potential rewards. - /// 0 if the action is executed directly by the user, without a facilitator. - function depositETHReferral(address receiver, uint16 referralCode) - external - payable - override - whenNotPaused // F:[P4-4] - // nonReentrant moved to internal function to make check for receive() and this function in one place - wethPoolOnly // F:[P4-6] - returns (uint256 shares) - { - shares = _depositETHReferral(receiver, referralCode); // F:[P4-7] - } - - /// @dev Sending ETH directly works - /// TODO: check if someone will use pool address during CA closure or liquidation with ETH transfer - receive() - external - payable - wethPoolOnly // F:[P4-5] - whenNotPaused // F:[P4-4] + returns ( + // nonReentrancy is set for deposit function + uint256 shares + ) { - if (msg.sender != wethAddress) { - _depositETHReferral(msg.sender, 0); // F:[P4-7] - } + shares = deposit(assets, receiver); // F:[P4-5] + emit DepositReferral(msg.sender, receiver, assets, referralCode); // F:[P4-5] } - /// @dev Mints exactly shares Vault shares to receiver by depositing assets of underlying tokens. - // MUST emit the Deposit event. - // MUST support EIP-20 approve / transferFrom on asset as a mint flow. MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint execution, and are accounted for during mint. - // MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not approving enough underlying tokens to the Vault contract, etc). - // Note that most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + /// @dev See {IERC4626-mint}. + /// + /// As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero. + /// In this case, the shares will be minted without requiring any assets to be deposited. function mint(uint256 shares, address receiver) - external - override + public + override(ERC4626, IERC4626) whenNotPaused // F:[P4-4] nonReentrant + nonZeroAddress(receiver) returns (uint256 assets) { - // Additional check that receiver is not address(0) - if (receiver == address(0)) revert ZeroAddressException(); - // No need to check for rounding error, previewMint rounds up. - assets = previewMint(shares); // F:[P4-4] - - uint256 assetsTransferred = _safeDepositAssets(assets); // F:[P4-8] - - /// TODO: add check for fee token - if (assetsTransferred != assets) { - shares = convertToShares(assetsTransferred); // F:[P4-8] - } - - _addLiquidity(receiver, assets, assetsTransferred, shares); // F:[P4-8] - } - - // - // LIQUIDITY INTERNAL - // - function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { - if (receiver == address(0)) revert ZeroAddressException(); - - uint256 assetsReceived = _safeDepositAssets(assets); - shares = previewDeposit(assetsReceived); + assets = previewMint(shares); // F:[P4-6,7] - _addLiquidity(receiver, assets, assetsReceived, shares); + _deposit(receiver, assets, _amountMinusFee(assets), shares); // F:[P4-6,7] } - function _depositETHReferral(address receiver, uint16 referralCode) - internal - nonReentrant // non-reentrancy check moved here to be able to use it in receive() and depositWithdrawal functions - returns (uint256 shares) - { - IWETH(wethAddress).deposit{value: msg.value}(); - - uint256 assets = msg.value; - shares = convertToShares(assets); - - _addLiquidity(receiver, assets, assets, shares); - emit DepositReferral(msg.sender, receiver, assets, referralCode); // T:[PS-2, 7] - } - - function _addLiquidity(address receiver, uint256 assetsSent, uint256 assetsReceived, uint256 shares) internal { + function _deposit(address receiver, uint256 assetsSent, uint256 assetsDelivered, uint256 shares) internal { /// Interst rate calculatiuon?? - if (expectedLiquidity() + assetsReceived > uint256(_expectedLiquidityLimit)) { - revert ExpectedLiquidityLimitException(); + if (expectedLiquidity() + assetsDelivered > uint256(_expectedLiquidityLimit)) { + revert ExpectedLiquidityLimitException(); // F:[P4-7] } - int256 assetsSigned = int256(assetsReceived); // F:[P4-4] - - _updateBaseParameters(assetsSigned, assetsSigned, false); // F:[P4-4] - - _mint(receiver, shares); // F:[P4-4] + int256 assetsDeliveredSgn = int256(assetsDelivered); // F:[P4-5,6] - emit Deposit(msg.sender, receiver, assetsSent, shares); // F:[P4-4] - } + /// @dev available liquidity is 0, because assets are already transffered + /// It's updated after transfer to account real asset delivered to account + _updateBaseParameters(assetsDeliveredSgn, assetsDeliveredSgn, false); // F:[P4-5,6] - function _safeDepositAssets(uint256 amount) internal returns (uint256) { - return _safeUnderlyingTransfer(address(this), amount); - } + IERC20(underlyingToken).safeTransferFrom(msg.sender, address(this), assetsSent); - function _safeUnderlyingTransfer(address to, uint256 amount) internal virtual returns (uint256) { - IERC20(underlyingToken).safeTransferFrom(msg.sender, to, amount); - return amount; - } + _mint(receiver, shares); // F:[P4-5,6] - function _amountWithFee(uint256 amount) internal view virtual returns (uint256) { - return amount; + emit Deposit(msg.sender, receiver, assetsSent, shares); // F:[P4-5,6] } - /// @dev Removes liquidity from pool - /// Burns shares from owner and sends exactly assets of underlying tokens to receiver. - // MUST emit the Withdraw event. - // MUST support a withdraw flow where the shares are burned from owner directly where owner is msg.sender. - // MUST support a withdraw flow where the shares are burned from owner directly where msg.sender has EIP-20 approval over the shares of owner. - // MAY support an additional flow in which the shares are transferred to the Vault contract before the withdraw execution, and are accounted for during withdraw. - // SHOULD check msg.sender can spend owner funds, assets needs to be converted to shares and shares should be checked for allowance. - // MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc). - // Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. Those methods should be performed separately. + /// @dev See {IERC4626-withdraw}. function withdraw(uint256 assets, address receiver, address owner) - external - override + public + override(ERC4626, IERC4626) whenNotPaused // F:[P4-4] nonReentrant + nonZeroAddress(receiver) returns (uint256 shares) { - shares = previewWithdraw(assets); - _removeLiquidity(assets, shares, receiver, owner, false); + // @dev it returns share taking fee into account + shares = previewWithdraw(assets); // F:[P4-8] + _withdraw(assets, _convertToAssets(shares, Math.Rounding.Down), shares, receiver, owner); // F:[P4-8] } - function withdrawETH(uint256 assets, address receiver, address owner) - external - override - whenNotPaused // F:[P4-4] - nonReentrant - wethPoolOnly // F:[P4-5] - returns (uint256 shares) - { - shares = previewWithdraw(assets); - _removeLiquidity(assets, shares, receiver, owner, true); - } - - /// @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. - // MUST emit the Withdraw event. - // MUST support a redeem flow where the shares are burned from owner directly where owner is msg.sender. - // MUST support a redeem flow where the shares are burned from owner directly where msg.sender has EIP-20 approval over the shares of owner. - // MAY support an additional flow in which the shares are transferred to the Vault contract before the redeem execution, and are accounted for during redeem. - // SHOULD check msg.sender can spend owner funds using allowance. - // MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner not having enough shares, etc). - // Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. Those methods should be performed separately. + /// @dev See {IERC4626-redeem}. function redeem(uint256 shares, address receiver, address owner) - external - override - whenNotPaused // F:[P4-4] - nonReentrant - returns (uint256 assets) - { - assets = convertToAssets(shares); - - // Check for rounding error since we round down in previewRedeem. - if (assets == 0) revert ZeroAssetsException(); - - _removeLiquidity(assets, shares, receiver, owner, false); - } - - function redeemETH(uint256 shares, address receiver, address owner) - external - override + public + override(ERC4626, IERC4626) whenNotPaused // F:[P4-4] nonReentrant - wethPoolOnly // F:[P4-5] - returns (uint256 assets) + nonZeroAddress(receiver) + returns (uint256 assetsDelivered) { - assets = convertToAssets(shares); - - // Check for rounding error since we round down in previewRedeem. - if (assets == 0) revert ZeroAssetsException(); + /// Note: Computes assets without fees + uint256 assetsSpent = _convertToAssets(shares, Math.Rounding.Down); // F:[P4-9] + assetsDelivered = _calcDeliveredAsstes(assetsSpent); // F:[P4-9] - _removeLiquidity(assets, shares, receiver, owner, true); + _withdraw(assetsDelivered, assetsSpent, shares, receiver, owner); // F:[P4-9] } - function _removeLiquidity(uint256 assets, uint256 shares, address receiver, address owner, bool convertWETH) + /// @dev Withdraw/redeem common workflow. + function _withdraw(uint256 assetsDelivered, uint256 assetsSpent, uint256 shares, address receiver, address owner) internal { - if (receiver == address(0)) revert ZeroAddressException(); - if (msg.sender != owner) { - uint256 allowed = allowance(owner, msg.sender); // Saves gas for limited approvals. - - if (allowed != type(uint256).max) { - _spendAllowance(owner, msg.sender, shares); - } + _spendAllowance(owner, msg.sender, shares); // F:[P4-8,9] } - _updateBaseParameters(-int256(assets), -int256(assets), false); - - if (withdrawFee > 0) { - unchecked { - /// It's safe because we made a check that assets < uint128, and withDrawFee is < 10K - uint256 withdrawFeeAmount = (assets * withdrawFee) / PERCENTAGE_FACTOR; - assets -= withdrawFeeAmount; - - IERC20(underlyingToken).safeTransfer(treasuryAddress, withdrawFeeAmount); - } - } + _updateBaseParameters(-int256(assetsSpent), -int256(assetsSpent), false); // F:[P4-8,9] - _burn(msg.sender, shares); + _burn(owner, shares); // F:[P4-8,9] - _withdrawAssets(receiver, assets, convertWETH); + uint256 amountToUser = _amountWithFee(assetsDelivered); // F:[P4-8,9] - emit Withdraw(msg.sender, receiver, owner, assets, shares); - } + IERC20(underlyingToken).safeTransfer(receiver, amountToUser); // F:[P4-8,9] - /// @dev Send assets back to user - function _withdrawAssets(address receiver, uint256 assets, bool convertWETH) internal virtual { - if (convertWETH) { - _unwrapWETH(receiver, assets); - } else { - IERC20(underlyingToken).safeTransfer(receiver, assets); + if (assetsSpent > amountToUser) { + unchecked { + IERC20(underlyingToken).safeTransfer(treasury, assetsSpent - amountToUser); // F:[P4-8,9] + } } - } - /// @dev Internal implementation for unwrapETH - function _unwrapWETH(address to, uint256 amount) internal { - IWETH(wethAddress).withdraw(amount); - payable(to).sendValue(amount); + emit Withdraw(msg.sender, receiver, owner, assetsDelivered, shares); // F:[P4-8, 9] } function burn(uint256 shares) @@ -458,154 +305,120 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { whenNotPaused // TODO: Add test nonReentrant { - _burn(msg.sender, shares); + _burn(msg.sender, shares); // F:[P4-10] + } + + // + // FEE TOKEN SUPPORT + + function _amountWithFee(uint256 amount) internal view virtual returns (uint256) { + return amount; + } + + function _amountMinusFee(uint256 amount) internal view virtual returns (uint256) { + return amount; } // // ACCOUNTING LOGIC // - /// @dev Returns the current exchange rate of Diesel tokens to underlying - function getDieselRate_RAY() public view override returns (uint256) { - if (totalSupply() == 0) return RAY; // F:[P4-1] - - return (uint256(expectedLiquidity()) * RAY) / totalSupply(); // F:[P4-4] + /** + * @dev Internal conversion function (from assets to shares) with support for rounding direction. + * + * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset + * would represent an infinite amount of shares. + */ + function _convertToShares(uint256 assets, Math.Rounding rounding) internal view override returns (uint256 shares) { + uint256 supply = totalSupply(); + return (assets == 0 || supply == 0) ? assets : assets.mulDiv(supply, expectedLiquidity(), rounding); } - /// @dev The address of the underlying token used for the Vault for accounting, depositing, and withdrawing. - function asset() external view returns (address) { - return underlyingToken; + /** + * @dev Internal conversion function (from shares to assets) with support for rounding direction. + */ + function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view override returns (uint256 assets) { + uint256 supply = totalSupply(); + return (supply == 0) ? shares : shares.mulDiv(expectedLiquidity(), supply, rounding); } - /// @dev Total amount of the underlying asset that is “managed” by Vault. - function totalAssets() external view returns (uint256 assets) { + /// @dev See {IERC4626-totalAssets}. + function totalAssets() public view override(ERC4626, IERC4626) returns (uint256 assets) { return expectedLiquidity(); } - /// @dev Return diesel token decimals - function decimals() public view virtual override (ERC20, IERC20Metadata) returns (uint8) { - return _decimals; - } - - /// @dev Converts a quantity of the underlying to Diesel tokens - /// The amount of shares that the Vault would exchange for the amount of assets provided, in an ideal scenario where all the conditions are met. - // MUST NOT be inclusive of any fees that are charged against assets in the Vault. - // MUST NOT show any variations depending on the caller. - // MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - // MUST NOT revert unless due to integer overflow caused by an unreasonably large input. - // MUST round down towards 0. - // This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and from. - /// @param assets Amount in underlyingToken tokens to be converted to diesel tokens - function convertToShares(uint256 assets) public view override returns (uint256 shares) { - return (assets * RAY) / getDieselRate_RAY(); - } - - /// @dev Converts a quantity of Diesel tokens to the underlying - /// The amount of assets that the Vault would exchange for the amount of shares provided, in an ideal scenario where all the conditions are met. - // MUST NOT be inclusive of any fees that are charged against assets in the Vault. - // MUST NOT show any variations depending on the caller. - // MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. - // MUST NOT revert unless due to integer overflow caused by an unreasonably large input. - // MUST round down towards 0. - // This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and from. - /// @param shares Amount in diesel tokens to be converted to diesel tokens - function convertToAssets(uint256 shares) public view override returns (uint256 assets) { - return (shares * getDieselRate_RAY()) / RAY; // T:[PS-24] - } - - /// @dev Maximum amount of the underlying asset that can be deposited into the Vault for the receiver, through a deposit call. - // MUST return the maximum amount of assets deposit would allow to be deposited for receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf of asset. - // MUST factor in both global and user-specific limits, like if deposits are entirely disabled (even temporarily) it MUST return 0. - // MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. - // MUST NOT revert. - - function maxDeposit(address) external view returns (uint256) { + /// @dev See {IERC4626-maxDeposit}. + function maxDeposit(address) public view override(ERC4626, IERC4626) returns (uint256) { return (_expectedLiquidityLimit == type(uint128).max) ? type(uint256).max - : _expectedLiquidityLimit - expectedLiquidity(); + : _amountWithFee(_expectedLiquidityLimit - expectedLiquidity()); } - /// Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions. - // MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called in the same transaction. - // MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the deposit would be accepted, regardless if the user has enough tokens approved, etc. - // MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. - // MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause deposit to revert. - // Note that any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by depositing. - function previewDeposit(uint256 assets) public view override returns (uint256) { - return convertToShares(assets); + /// @dev See {IERC4626-previewDeposit}. + function previewDeposit(uint256 assets) public view override(ERC4626, IERC4626) returns (uint256) { + return _convertToShares(_amountMinusFee(assets), Math.Rounding.Down); // TODO: add fee parameter } - /// @dev Maximum amount of shares that can be minted from the Vault for the receiver, through a mint call. - /// MUST return the maximum amount of shares mint would allow to be deposited to receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). This assumes that the user has infinite assets, i.e. MUST NOT rely on balanceOf of asset. - /// MUST factor in both global and user-specific limits, like if mints are entirely disabled (even temporarily) it MUST return 0. - /// MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. - /// MUST NOT revert. - - function maxMint(address) external view returns (uint256) { + /// @dev See {IERC4626-maxMint}. + function maxMint(address) public view override(ERC4626, IERC4626) returns (uint256) { uint128 limit = _expectedLiquidityLimit; - return (limit == type(uint128).max) ? type(uint256).max : convertToShares(limit - expectedLiquidity()); + return (limit == type(uint128).max) ? type(uint256).max : previewMint(limit - expectedLiquidity()); } - /// @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given current on-chain conditions. - /// MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the same transaction. - /// MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint would be accepted, regardless if the user has enough tokens approved, etc. - /// MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. - /// MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause mint to revert. - /// Note that any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by minting. - function previewMint(uint256 shares) public view virtual returns (uint256) { - uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. - - return _amountWithFee(supply == 0 ? shares : convertToAssets(shares)); // We need to round up shares.mulDivUp(totalAssets(), supply); + /// @dev See {IERC4626-previewMint}. + function previewMint(uint256 shares) public view override(ERC4626, IERC4626) returns (uint256) { + return _amountWithFee(_convertToAssets(shares, Math.Rounding.Up)); // We need to round up shares.mulDivUp(totalAssets(), supply); } - /// @dev Maximum amount of the underlying asset that can be withdrawn from the owner balance in the Vault, through a withdraw call. - /// MUST return the maximum amount of assets that could be transferred from owner through withdraw and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). - /// MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0. - /// MUST NOT revert. - function maxWithdraw(address owner) external view returns (uint256) { + /// @dev See {IERC4626-maxWithdraw}. + function maxWithdraw(address owner) public view override(ERC4626, IERC4626) returns (uint256) { return availableLiquidity().min(previewWithdraw(balanceOf(owner))); } - /// @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, given current on-chain conditions. - // MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if called in the same transaction. - // MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though the withdrawal would be accepted, regardless if the user has enough shares, etc. - // MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. - // MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause withdraw to revert. - // Note that any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by depositing. - function previewWithdraw(uint256 assets) public view override returns (uint256) { - uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. - return supply == 0 ? assets : assets.mulDivUp(supply, expectedLiquidity()); + /// @dev See {IERC4626-previewWithdraw}. + function previewWithdraw(uint256 assets) public view override(ERC4626, IERC4626) returns (uint256) { + return _convertToShares( + _amountWithFee(assets) * PERCENTAGE_FACTOR / (PERCENTAGE_FACTOR - withdrawFee), Math.Rounding.Up + ); } - /// @dev Maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, through a redeem call. - // MUST return the maximum amount of shares that could be transferred from owner through redeem and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). - // MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0. - // MUST NOT revert. - function maxRedeem(address owner) external view returns (uint256 shares) { + /// @dev See {IERC4626-maxRedeem}. + function maxRedeem(address owner) public view override(ERC4626, IERC4626) returns (uint256 shares) { shares = balanceOf(owner); - uint256 assets = convertToAssets(shares); + uint256 assets = _convertToAssets(shares, Math.Rounding.Down); uint256 assetsAvailable = availableLiquidity(); if (assets > assetsAvailable) { - shares = previewWithdraw(assetsAvailable); + shares = _convertToShares(assetsAvailable, Math.Rounding.Down); } } - /// @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, given current on-chain conditions. - // MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the same transaction. - // MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the redemption would be accepted, regardless if the user has enough shares, etc. - // MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. - // MUST NOT revert due to vault specific user/global limits. MAY revert due to other conditions that would also cause redeem to revert. - // Note that any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in share price or some other type of condition, meaning the depositor will lose assets by redeeming. - function previewRedeem(uint256 shares) public view override returns (uint256) { - return convertToAssets(shares); + /// @dev See {IERC4626-previewRedeem}. + function previewRedeem(uint256 shares) public view override(ERC4626, IERC4626) returns (uint256 assets) { + assets = _calcDeliveredAsstes(_convertToAssets(shares, Math.Rounding.Down)); + } + + /// @dev Computes how much assets will de delivered takling intio account token fees & withdraw fee + function _calcDeliveredAsstes(uint256 assetsSpent) internal view returns (uint256) { + uint256 assetsDelivered = assetsSpent; + + if (withdrawFee > 0) { + unchecked { + /// It's safe because we made a check that assetsDelivered < uint128, and withDrawFee is < 10K + uint256 withdrawFeeAmount = (assetsDelivered * withdrawFee) / PERCENTAGE_FACTOR; + assetsDelivered -= withdrawFeeAmount; + } + } + + return _amountMinusFee(assetsDelivered); } - /// @return expected liquidity - the amount of money that should be in the pool + /// @return Amount of money that should be in the pool /// after all users close their Credit accounts and fully repay debts function expectedLiquidity() public view override returns (uint256) { return _expectedLiquidityLU + _calcBaseInterestAccrued() + (supportsQuotas ? _calcOutstandingQuotaRevenue() : 0); // } + /// @dev Computes interest rate accrued from last update (LU) function _calcBaseInterestAccrued() internal view returns (uint256) { // timeDifference = blockTime - previous timeStamp uint256 timeDifference = block.timestamp - timestampLU; @@ -614,7 +427,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // interestAccrued = totalBorrow * ------------------------------------ // SECONDS_PER_YEAR // - return (uint256(_totalBorrowed) * _borrowRate_RAY * timeDifference) / RAY / SECONDS_PER_YEAR; + return (uint256(_totalBorrowed) * _borrowRate * timeDifference) / RAY / SECONDS_PER_YEAR; } function _calcOutstandingQuotaRevenue() internal view returns (uint128) { @@ -679,21 +492,21 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // Updates credit manager specific totalBorrowed CreditManagerDebt storage cmDebt = creditManagersDebt[msg.sender]; - uint128 totalBorrowed = cmDebt.totalBorrowed; - if (totalBorrowed == 0) { + uint128 cmTotalBorrowed = cmDebt.totalBorrowed; + if (cmTotalBorrowed == 0) { /// todo: add correct exception ?? revert CreditManagerOnlyException(); } // For fee surplus we mint tokens for treasury if (profit > 0) { - _mint(treasuryAddress, convertToShares(profit)); + _mint(treasury, convertToShares(profit)); } else { // If returned money < borrowed amount + interest accrued // it tries to compensate loss by burning diesel (LP) tokens // from treasury fund uint256 sharesToBurn = convertToShares(loss); - uint256 sharesInTreasury = balanceOf(treasuryAddress); + uint256 sharesInTreasury = balanceOf(treasury); if (sharesInTreasury < sharesToBurn) { sharesToBurn = sharesInTreasury; @@ -702,7 +515,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // If treasury has enough funds, it just burns needed amount // to keep diesel rate on the same level - _burn(treasuryAddress, sharesToBurn); + _burn(treasury, sharesToBurn); } // Updates borrow rate @@ -711,7 +524,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // Updates total borrowed _totalBorrowed -= uint128(borrowedAmount); - cmDebt.totalBorrowed = totalBorrowed - uint128(borrowedAmount); + cmDebt.totalBorrowed = cmTotalBorrowed - uint128(borrowedAmount); emit Repay(msg.sender, borrowedAmount, profit, loss); } @@ -731,14 +544,14 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { //solium-disable-next-line uint256 timeDifference = block.timestamp - timestampLU; - return calcLinearIndex_RAY(cumulativeIndexLU_RAY, _borrowRate_RAY, timeDifference); + return calcLinearIndex_RAY(cumulativeIndexLU_RAY, _borrowRate, timeDifference); } /// @dev Calculates a new cumulative index value from the initial value, borrow rate and time elapsed /// @param cumulativeIndex_RAY Cumulative index at last update, in RAY - /// @param currentBorrowRate_RAY Current borrow rate, in RAY + /// @param currentborrowRate Current borrow rate, in RAY /// @param timeDifference Time elapsed since last update, in seconds - function calcLinearIndex_RAY(uint256 cumulativeIndex_RAY, uint256 currentBorrowRate_RAY, uint256 timeDifference) + function calcLinearIndex_RAY(uint256 cumulativeIndex_RAY, uint256 currentborrowRate, uint256 timeDifference) public pure returns (uint256) @@ -747,7 +560,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // newIndex = currentIndex * | 1 + ------------------------------------ | // \ SECONDS_PER_YEAR / // - uint256 linearAccumulated_RAY = RAY + (currentBorrowRate_RAY * timeDifference) / SECONDS_PER_YEAR; + uint256 linearAccumulated_RAY = RAY + (currentborrowRate * timeDifference) / SECONDS_PER_YEAR; return (cumulativeIndex_RAY * linearAccumulated_RAY) / RAY; } @@ -758,19 +571,20 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { int256 availableLiquidityChanged, bool checkOptimalBorrowing ) internal { - uint128 expectedLiquidityLUcached = uint128( + uint128 updatedExpectedLiquidityLU = uint128( int128(_expectedLiquidityLU + uint128(_calcBaseInterestAccrued())) + int128(expectedLiquidityChanged) ); - _expectedLiquidityLU = expectedLiquidityLUcached; + _expectedLiquidityLU = updatedExpectedLiquidityLU; // Update cumulativeIndex cumulativeIndexLU_RAY = uint128(calcLinearCumulative_RAY()); // update borrow APY - _borrowRate_RAY = uint128( + // TODO: add case to check with quotas + _borrowRate = uint128( interestRateModel.calcBorrowRate( - expectedLiquidityLUcached + (supportsQuotas ? _calcOutstandingQuotaRevenue() : 0), + updatedExpectedLiquidityLU + (supportsQuotas ? _calcOutstandingQuotaRevenue() : 0), availableLiquidityChanged == 0 ? availableLiquidity() : uint256(int256(availableLiquidity()) + availableLiquidityChanged), @@ -799,8 +613,8 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { // GETTERS /// @dev Calculates the current borrow rate, RAY format - function borrowRate_RAY() external view returns (uint256) { - return uint256(_borrowRate_RAY); + function borrowRate() external view returns (uint256) { + return uint256(_borrowRate); } /// @dev Total borrowed amount (includes principal only) @@ -816,7 +630,11 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { /// @dev Forbids a Credit Manager to borrow /// @param _creditManager Address of the Credit Manager - function setCreditManagerLimit(address _creditManager, uint256 _limit) external controllerOnly { + function setCreditManagerLimit(address _creditManager, uint256 _limit) + external + controllerOnly + nonZeroAddress(_creditManager) + { /// Reverts if _creditManager is not registered in ContractRE#gister if (!ContractsRegister(AddressProvider(addressProvider).getContractsRegister()).isCreditManager(_creditManager)) { @@ -835,7 +653,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { } CreditManagerDebt storage cmDebt = creditManagersDebt[_creditManager]; - cmDebt.limit = convertToU128(_limit); + cmDebt.limit = _convertToU128(_limit); emit BorrowLimitChanged(_creditManager, _limit); } @@ -844,9 +662,8 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { function updateInterestRateModel(address _interestRateModel) public configuratorOnly // T:[PS-9] + nonZeroAddress(_interestRateModel) { - if (_interestRateModel == address(0)) revert ZeroAddressException(); // F:[P4-2] - interestRateModel = IInterestRateModel(_interestRateModel); _updateBaseParameters(0, 0, false); @@ -854,14 +671,30 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { emit NewInterestRateModel(_interestRateModel); // F:[P4-03] } + /// @dev Sets the new pool quota keeper + /// @param _poolQuotaKeeper Address of the new poolQuotaKeeper copntract + function connectPoolQuotaManager(address _poolQuotaKeeper) + public + configuratorOnly // T:[PS-9] + nonZeroAddress(_poolQuotaKeeper) + { + if (poolQuotaKeeper != address(0)) { + _updateQuotaRevenue(quotaRevenue); + } + + poolQuotaKeeper = _poolQuotaKeeper; + + emit NewPoolQuotaKeeper(_poolQuotaKeeper); // F:[P4-03] + } + /// @dev Sets a new expected liquidity limit /// @param limit New expected liquidity limit function setExpectedLiquidityLimit(uint256 limit) external controllerOnly { - _setExpectedLiquidityLimit(limit); + _setExpectedLiquidityLimit(limit); // F:[P4-7] } function _setExpectedLiquidityLimit(uint256 limit) internal { - _expectedLiquidityLimit = convertToU128(limit); + _expectedLiquidityLimit = _convertToU128(limit); emit NewExpectedLiquidityLimit(limit); // F:[P4-03] } @@ -870,7 +703,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { } function _setTotalBorrowedLimit(uint256 limit) internal { - _totalBorrowedLimit = convertToU128(limit); + _totalBorrowedLimit = _convertToU128(limit); emit NewTotalBorrowedLimit(limit); // F:[P4-03] } @@ -887,6 +720,9 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { emit NewWithdrawFee(_withdrawFee); // T:[PS-33] } + // + // GETTERS + // function creditManagers() external view returns (address[] memory) { return creditManagerSet.values(); } @@ -900,7 +736,7 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { /// @dev Borrow limit for particular credit manager function creditManagerLimit(address _creditManager) external view returns (uint256) { CreditManagerDebt storage cmDebt = creditManagersDebt[_creditManager]; - return convertToU256(cmDebt.limit); + return _convertToU256(cmDebt.limit); } /// @dev How much current credit manager can borrow @@ -931,22 +767,25 @@ contract Pool4626 is ERC20, IPool4626, ACLNonReentrantTrait { } function expectedLiquidityLimit() external view override returns (uint256) { - return convertToU256(_expectedLiquidityLimit); + return _convertToU256(_expectedLiquidityLimit); } function expectedLiquidityLU() external view returns (uint256) { - return convertToU256(_expectedLiquidityLU); + return _convertToU256(_expectedLiquidityLU); } function totalBorrowedLimit() external view override returns (uint256) { - return convertToU256(_totalBorrowedLimit); + return _convertToU256(_totalBorrowedLimit); } - function convertToU256(uint128 limit) internal pure returns (uint256) { + // + // INTERNAL HELPERS + // + function _convertToU256(uint128 limit) internal pure returns (uint256) { return (limit == type(uint128).max) ? type(uint256).max : limit; } - function convertToU128(uint256 limit) internal pure returns (uint128) { + function _convertToU128(uint256 limit) internal pure returns (uint128) { return (limit == type(uint256).max) ? type(uint128).max : uint128(limit); } } diff --git a/contracts/pool/Pool4626_USDT.sol b/contracts/pool/Pool4626_USDT.sol index c4d9506..b1ee88d 100644 --- a/contracts/pool/Pool4626_USDT.sol +++ b/contracts/pool/Pool4626_USDT.sol @@ -15,11 +15,11 @@ contract Pool4626_USDT is Pool4626, USDT_Transfer { // Additional check that receiver is not address(0) } - function _safeUnderlyingTransfer(address to, uint256 amount) internal override returns (uint256) { - return _safeUSDTTransfer(to, amount); - } - function _amountWithFee(uint256 amount) internal view override returns (uint256) { return _amountUSDTWithFee(amount); } + + function _amountMinusFee(uint256 amount) internal view override returns (uint256) { + return _amountUSDTMinusFee(amount); + } } diff --git a/contracts/pool/PoolQuotaKeeper.sol b/contracts/pool/PoolQuotaKeeper.sol index c179588..0211694 100644 --- a/contracts/pool/PoolQuotaKeeper.sol +++ b/contracts/pool/PoolQuotaKeeper.sol @@ -33,7 +33,6 @@ import {IGauge} from "../interfaces/IGauge.sol"; import {RAY, PERCENTAGE_FACTOR, SECONDS_PER_YEAR, MAX_WITHDRAW_FEE} from "../libraries/Constants.sol"; import {Errors} from "../libraries/Errors.sol"; -import {FixedPointMathLib} from "../libraries/SolmateMath.sol"; // EXCEPTIONS import {ZeroAddressException} from "../interfaces/IErrors.sol"; @@ -92,7 +91,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait { /// @dev Constructor /// @param _pool Pool address - constructor(address payable _pool) ACLNonReentrantTrait(address(Pool4626(_pool).addressProvider())) { + constructor(address _pool) ACLNonReentrantTrait(address(Pool4626(_pool).addressProvider())) { // Additional check that receiver is not address(0) if (_pool == address(0)) { revert ZeroAddressException(); // F:[P4-02] diff --git a/contracts/test/helpers/BalanceEngine.sol b/contracts/test/helpers/BalanceEngine.sol index 50642b9..6308072 100644 --- a/contracts/test/helpers/BalanceEngine.sol +++ b/contracts/test/helpers/BalanceEngine.sol @@ -21,7 +21,7 @@ contract BalanceEngine is DSTest { if (balance < minBalance) { emit log_named_address( string( - abi.encodePacked(reason, "Insufficient ", IERC20Metadata(token).symbol(), " balance on account: ") + abi.encodePacked(reason, "\nInsufficient ", IERC20Metadata(token).symbol(), " balance on account: ") ), holder ); @@ -35,7 +35,9 @@ contract BalanceEngine is DSTest { if (balance > maxBalance) { emit log_named_address( - string(abi.encodePacked(reason, "Exceeding ", IERC20Metadata(token).symbol(), " balance on account: ")), + string( + abi.encodePacked(reason, "\nExceeding ", IERC20Metadata(token).symbol(), " balance on account: ") + ), holder ); } @@ -48,7 +50,9 @@ contract BalanceEngine is DSTest { if (balance != expectedBalance) { emit log_named_address( - string(abi.encodePacked(reason, "Incorrect ", IERC20Metadata(token).symbol(), " balance on account: ")), + string( + abi.encodePacked(reason, "\nIncorrect ", IERC20Metadata(token).symbol(), " balance on account: ") + ), holder ); } diff --git a/contracts/test/pool/Pool4626.t.sol b/contracts/test/pool/Pool4626.t.sol index 9ba8697..2119ce7 100644 --- a/contracts/test/pool/Pool4626.t.sol +++ b/contracts/test/pool/Pool4626.t.sol @@ -6,11 +6,14 @@ pragma solidity ^0.8.10; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Pool4626} from "../../pool/Pool4626.sol"; import {IERC4626Events} from "../../interfaces/IERC4626.sol"; import {IPool4626Events, Pool4626Opts, IPool4626Exceptions} from "../../interfaces/IPool4626.sol"; -import {LinearInterestRateModel} from "../../pool/LinearInterestRateModel.sol"; +import {IERC4626Events} from "../../interfaces/IERC4626.sol"; + +import {IInterestRateModel} from "../../interfaces/IInterestRateModel.sol"; import {ACL} from "../../core/ACL.sol"; import {CreditManagerMockForPoolTest} from "../mocks/pool/CreditManagerMockForPoolTest.sol"; @@ -31,6 +34,7 @@ import {ERC20FeeMock} from "../mocks/token/ERC20FeeMock.sol"; // TEST import "../lib/constants.sol"; +import "../lib/StringUtils.sol"; import {PERCENTAGE_FACTOR} from "../../libraries/PercentageMath.sol"; import "forge-std/console.sol"; @@ -47,6 +51,9 @@ uint256 constant fee = 6000; /// @title pool /// @notice Business logic for borrowing liquidity pools contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events { + using Math for uint256; + using StringUtils for string; + CheatCodes evm = CheatCodes(HEVM_ADDRESS); PoolServiceTestSuite psts; @@ -63,6 +70,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events Pool4626 pool; address underlying; CreditManagerMockForPoolTest cmMock; + IInterestRateModel irm; function setUp() public { _setUp(Tokens.DAI); @@ -73,10 +81,12 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events psts = new PoolServiceTestSuite( tokenTestSuite, tokenTestSuite.addressOf(t), - true + true, + false ); pool = psts.pool4626(); + irm = psts.linearIRModel(); underlying = address(psts.underlying()); cmMock = psts.cmMock(); acl = psts.acl(); @@ -85,17 +95,46 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // // HELPERS // + function _setUpTestCase( + Tokens t, + uint256 feeToken, + uint16 utilisation, + uint256 availableLiquidity, + uint256 dieselRate, + uint16 withdrawFee + ) internal { + _setUp(t); + if (t == Tokens.USDT) { + // set 50% fee if fee token + ERC20FeeMock(pool.asset()).setMaximumFee(type(uint256).max); + ERC20FeeMock(pool.asset()).setBasisPointsRate(feeToken); + } + + _initPoolLiquidity(availableLiquidity, dieselRate); + _connectAndSetLimit(); + _borrow(utilisation); + + evm.prank(CONFIGURATOR); + pool.setWithdrawFee(withdrawFee); + } + function _connectAndSetLimit() internal { evm.prank(CONFIGURATOR); pool.setCreditManagerLimit(address(cmMock), type(uint128).max); } - function _mulFee(uint256 amount, uint256 fee) internal returns (uint256) { - return (amount * (PERCENTAGE_FACTOR - fee)) / PERCENTAGE_FACTOR; + function _borrow(uint16 utilisation) internal { + cmMock.lendCreditAccount(pool.expectedLiquidity() / 2, DUMB_ADDRESS); + + assertEq(pool.borrowRate(), irm.calcBorrowRate(PERCENTAGE_FACTOR, utilisation, false)); + } + + function _mulFee(uint256 amount, uint256 _fee) internal returns (uint256) { + return (amount * (PERCENTAGE_FACTOR - _fee)) / PERCENTAGE_FACTOR; } - function _divFee(uint256 amount, uint256 fee) internal returns (uint256) { - return (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - fee); + function _divFee(uint256 amount, uint256 _fee) internal returns (uint256) { + return (amount * PERCENTAGE_FACTOR) / (PERCENTAGE_FACTOR - _fee); } function _updateBorrowrate() internal { @@ -104,14 +143,24 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events } function _initPoolLiquidity() internal { + _initPoolLiquidity(addLiquidity, 2 * RAY); + } + + function _initPoolLiquidity(uint256 availableLiquidity, uint256 dieselRate) internal { + assertEq(pool.convertToAssets(RAY), RAY, "Incorrect diesel rate!"); + evm.prank(INITIAL_LP); - pool.mint(2 * addLiquidity, INITIAL_LP); + pool.mint(availableLiquidity, INITIAL_LP); evm.prank(INITIAL_LP); - pool.burn(addLiquidity); + pool.burn(availableLiquidity * (dieselRate - RAY) / dieselRate); + + // assertEq(pool.expectedLiquidityLU(), availableLiquidity * dieselRate / RAY, "ExpectedLU is not correct!"); + assertEq(pool.convertToAssets(RAY), dieselRate, "Incorrect diesel rate!"); + } - assertEq(pool.expectedLiquidityLU(), addLiquidity * 2, "ExpectedLU is not correct!"); - assertEq(pool.getDieselRate_RAY(), 2 * RAY, "Incorrect diesel rate!"); + function _testCaseErr(string memory caseName, string memory err) internal pure returns (string memory) { + return string("\nCase: ").concat(caseName).concat("\n").concat("Error: ").concat(err); } // @@ -129,19 +178,15 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events assertEq(pool.decimals(), IERC20Metadata(address(psts.underlying())).decimals(), "Incorrect decimals"); - assertEq(pool.treasuryAddress(), psts.addressProvider().getTreasuryContract(), "Incorrect treasury"); + assertEq(pool.treasury(), psts.addressProvider().getTreasuryContract(), "Incorrect treasury"); - assertEq(pool.getDieselRate_RAY(), RAY); + assertEq(pool.convertToAssets(RAY), RAY, "Incorrect diesel rate!"); assertEq(address(pool.interestRateModel()), address(psts.linearIRModel()), "Incorrect interest rate model"); assertEq(pool.expectedLiquidityLimit(), type(uint256).max); assertEq(pool.totalBorrowedLimit(), type(uint256).max); - - // assertTrue(!pool.isFeeToken(), "Incorrect isFeeToken"); - - assertEq(pool.wethAddress(), psts.addressProvider().getWethToken(), "Incorrect weth token"); } // [P4-2]: constructor reverts for zero addresses @@ -208,27 +253,15 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events evm.expectRevert(bytes(PAUSABLE_ERROR)); pool.depositReferral(addLiquidity, FRIEND, referral); - evm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.depositETHReferral(FRIEND, referral); - - evm.expectRevert(bytes(PAUSABLE_ERROR)); - payable(address(pool)).call{value: addLiquidity}(""); - evm.expectRevert(bytes(PAUSABLE_ERROR)); pool.mint(addLiquidity, FRIEND); evm.expectRevert(bytes(PAUSABLE_ERROR)); pool.withdraw(removeLiquidity, FRIEND, FRIEND); - evm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.withdrawETH(removeLiquidity, FRIEND, FRIEND); - evm.expectRevert(bytes(PAUSABLE_ERROR)); pool.redeem(removeLiquidity, FRIEND, FRIEND); - evm.expectRevert(bytes(PAUSABLE_ERROR)); - pool.redeemETH(removeLiquidity, FRIEND, FRIEND); - evm.expectRevert(bytes(PAUSABLE_ERROR)); pool.lendCreditAccount(1, FRIEND); @@ -238,230 +271,704 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events evm.stopPrank(); } - // [P4-5]: depositing eth work for WETH pools only - function test_P4_05_eth_functions_work_for_WETH_pools_only() public { - // adds liqudity to mint initial diesel tokens to change 1:1 rate - - evm.deal(USER, addLiquidity); - - evm.startPrank(USER); - - evm.expectRevert(IPool4626Exceptions.AssetIsNotWETHException.selector); - pool.depositETHReferral{value: addLiquidity}(FRIEND, referral); - - evm.expectRevert(IPool4626Exceptions.AssetIsNotWETHException.selector); - payable(address(pool)).call{value: addLiquidity}(""); - - evm.expectRevert(IPool4626Exceptions.AssetIsNotWETHException.selector); - pool.withdrawETH(1, FRIEND, USER); - - evm.expectRevert(IPool4626Exceptions.AssetIsNotWETHException.selector); - pool.redeemETH(1, FRIEND, USER); - - evm.stopPrank(); + struct DepositTestCase { + string name; + /// SETUP + Tokens asset; + uint256 tokenFee; + uint256 initialLiquidity; + uint256 dieselRate; + uint16 utilisation; + uint16 withdrawFee; + /// PARAMS + uint256 amountToDeposit; + /// EXPECTED VALUES + uint256 expectedShares; + uint256 expectedAvailableLiquidity; + uint256 expectedLiquidityAfter; } - // TODO: fix test - - // // [P4-6]: deposit adds liquidity correctly - // function test_P4_06_deposit_adds_liquidity_correctly() public { - // // adds liqudity to mint initial diesel tokens to change 1:1 rate - - // for (uint256 j; j < 2; ++j) { - // for (uint256 i; i < 2; ++i) { - // bool withReferralCode = j == 0; - - // bool feeToken = false; //i == 1; - - // _setUp(feeToken ? Tokens.USDT : Tokens.DAI); - - // if (feeToken) { - // // set 50% fee if fee token - // ERC20FeeMock(pool.asset()).setMaximumFee(type(uint256).max); - // ERC20FeeMock(pool.asset()).setBasisPointsRate(fee); - // } + // [P4-5]: deposit adds liquidity correctly + function test_P4_05_deposit_adds_liquidity_correctly() public { + // adds liqudity to mint initial diesel tokens to change 1:1 rate - // uint256 expectedShares = feeToken ? _mulFee(addLiquidity / 2, fee) : addLiquidity / 2; + DepositTestCase[2] memory cases = [ + DepositTestCase({ + name: "Normal token", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + amountToDeposit: addLiquidity, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedShares: addLiquidity / 2, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, + expectedLiquidityAfter: addLiquidity * 2 + }), + DepositTestCase({ + name: "Fee token", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + /// PARAMS + amountToDeposit: addLiquidity, + /// EXPECTED VALUES + expectedShares: (addLiquidity * 40 / 100) / 2, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity * 40 / 100, + expectedLiquidityAfter: addLiquidity + addLiquidity * 40 / 100 + }) + ]; + + for (uint256 i; i < cases.length; ++i) { + DepositTestCase memory testCase = cases[i]; + for (uint256 rc; rc < 2; ++rc) { + bool withReferralCode = rc == 0; + + _setUpTestCase( + testCase.asset, + testCase.tokenFee, + testCase.utilisation, + testCase.initialLiquidity, + testCase.dieselRate, + testCase.withdrawFee + ); - // evm.expectEmit(true, true, false, true); - // emit Transfer(address(0), FRIEND, expectedShares); + evm.expectEmit(true, true, false, true); + emit Transfer(address(0), FRIEND, testCase.expectedShares); - // evm.expectEmit(true, true, false, true); - // emit Deposit(USER, FRIEND, addLiquidity, expectedShares); + evm.expectEmit(true, true, false, true); + emit Deposit(USER, FRIEND, testCase.amountToDeposit, testCase.expectedShares); - // if (withReferralCode) { - // evm.expectEmit(true, true, false, true); - // emit DepositReferral(USER, FRIEND, addLiquidity, referral); - // } + if (withReferralCode) { + evm.expectEmit(true, true, false, true); + emit DepositReferral(USER, FRIEND, testCase.amountToDeposit, referral); + } - // evm.prank(USER); - // uint256 shares = withReferralCode - // ? pool.depositReferral(addLiquidity, FRIEND, referral) - // : pool.deposit(addLiquidity, FRIEND); + evm.prank(USER); + uint256 shares = withReferralCode + ? pool.depositReferral(testCase.amountToDeposit, FRIEND, referral) + : pool.deposit(testCase.amountToDeposit, FRIEND); + + expectBalance( + address(pool), + FRIEND, + testCase.expectedShares, + _testCaseErr(testCase.name, "Incorrect diesel tokens on FRIEND account") + ); + expectBalance(underlying, USER, liquidityProviderInitBalance - addLiquidity); + assertEq( + pool.expectedLiquidity(), + testCase.expectedLiquidityAfter, + _testCaseErr(testCase.name, "Incorrect expected liquidity") + ); + assertEq( + pool.availableLiquidity(), + testCase.expectedAvailableLiquidity, + _testCaseErr(testCase.name, "Incorrect available liquidity") + ); + assertEq(shares, testCase.expectedShares); + + assertEq( + pool.borrowRate(), + irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), + _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") + ); + } + } + } - // expectBalance(address(pool), FRIEND, expectedShares, "Incorrect diesel tokens on FRIEND account"); - // // expectBalance(underlying, USER, liquidityProviderInitBalance - addLiquidity); - // // assertEq(pool.expectedLiquidity(), addLiquidity * 3); - // // assertEq(pool.availableLiquidity(), addLiquidity * 3); - // // assertEq(shares, expectedShares); - // } - // } - // } + struct MintTestCase { + string name; + /// SETUP + Tokens asset; + uint256 tokenFee; + uint256 initialLiquidity; + uint256 dieselRate; + uint16 utilisation; + uint16 withdrawFee; + /// PARAMS + uint256 desiredShares; + /// EXPECTED VALUES + uint256 expectedAssetsWithdrawal; + uint256 expectedAvailableLiquidity; + uint256 expectedLiquidityAfter; + } - // [P4-7]: depositETH adds liquidity correctly - function test_P4_07_depositETH_adds_liquidity_correctly() public { - // adds liqudity to mint initial diesel tokens to change 1:1 rate + // [P4-6]: deposit adds liquidity correctly + function test_P4_06_mint_adds_liquidity_correctly() public { + MintTestCase[2] memory cases = [ + MintTestCase({ + name: "Normal token", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + desiredShares: addLiquidity / 2, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedAssetsWithdrawal: addLiquidity, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, + expectedLiquidityAfter: addLiquidity * 2 + }), + MintTestCase({ + name: "Fee token", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + /// PARAMS + desiredShares: addLiquidity / 2, + /// EXPECTED VALUES + /// fee token makes impact on how much tokens will be wiotdrawn from user + expectedAssetsWithdrawal: addLiquidity * 100 / 40, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity, + expectedLiquidityAfter: addLiquidity * 2 + }) + ]; + + for (uint256 i; i < cases.length; ++i) { + MintTestCase memory testCase = cases[i]; + + _setUpTestCase( + testCase.asset, + testCase.tokenFee, + testCase.utilisation, + testCase.initialLiquidity, + testCase.dieselRate, + testCase.withdrawFee + ); - for (uint256 i; i < 2; ++i) { - bool depositReferral = i == 0; + evm.expectEmit(true, true, false, true); + emit Transfer(address(0), FRIEND, testCase.desiredShares); - _setUp(Tokens.WETH); + evm.expectEmit(true, true, false, true); + emit Deposit(USER, FRIEND, testCase.expectedAssetsWithdrawal, testCase.desiredShares); - _initPoolLiquidity(); + evm.prank(USER); + uint256 assets = pool.mint(testCase.desiredShares, FRIEND); + + expectBalance( + address(pool), FRIEND, testCase.desiredShares, _testCaseErr(testCase.name, "Incorrect shares ") + ); + expectBalance( + underlying, + USER, + liquidityProviderInitBalance - testCase.expectedAssetsWithdrawal, + _testCaseErr(testCase.name, "Incorrect USER balance") + ); + assertEq( + pool.expectedLiquidity(), + testCase.expectedLiquidityAfter, + _testCaseErr(testCase.name, "Incorrect expected liquidity") + ); + assertEq( + pool.availableLiquidity(), + testCase.expectedAvailableLiquidity, + _testCaseErr(testCase.name, "Incorrect available liquidity") + ); + assertEq( + assets, testCase.expectedAssetsWithdrawal, _testCaseErr(testCase.name, "Incorrect assets return value") + ); + + assertEq( + pool.borrowRate(), + irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), + _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") + ); + } + } - uint256 expectedShares = addLiquidity / 2; + // [P4-7]: deposit and mint if assets more than limit + function test_P4_07_deposit_and_mint_if_assets_more_than_limit() public { + for (uint256 j; j < 2; ++j) { + for (uint256 i; i < 2; ++i) { + bool feeToken = i == 1; - address receiver = depositReferral ? FRIEND : USER; + Tokens asset = feeToken ? Tokens.USDT : Tokens.DAI; - evm.expectEmit(true, true, false, true); - emit Transfer(address(0), receiver, expectedShares); + _setUpTestCase(asset, feeToken ? 60_00 : 0, 50_00, addLiquidity, 2 * RAY, 0); - evm.expectEmit(true, true, false, true); - emit Deposit(USER, receiver, addLiquidity, expectedShares); + evm.prank(CONFIGURATOR); + pool.setExpectedLiquidityLimit(1237882323 * WAD); - if (depositReferral) { - evm.expectEmit(true, true, false, true); - emit DepositReferral(USER, FRIEND, addLiquidity, referral); - } + uint256 assetsToReachLimit = pool.expectedLiquidityLimit() - pool.expectedLiquidity(); - evm.deal(USER, addLiquidity); + uint256 sharesToReachLimit = assetsToReachLimit / 2; - uint256 shares; - if (depositReferral) { - evm.prank(USER); - shares = pool.depositETHReferral{value: addLiquidity}(FRIEND, referral); - } else { - evm.prank(USER); - payable(address(pool)).call{value: addLiquidity}(""); - } + if (feeToken) { + assetsToReachLimit = _divFee(assetsToReachLimit, fee); + } - expectBalance(address(pool), receiver, expectedShares); - assertEq(pool.expectedLiquidity(), addLiquidity * 3); - assertEq(pool.availableLiquidity(), addLiquidity * 3); + tokenTestSuite.mint(asset, USER, assetsToReachLimit + 1); - if (depositReferral) { - assertEq(shares, expectedShares); + if (j == 0) { + // DEPOSIT CASE + evm.prank(USER); + pool.deposit(assetsToReachLimit, FRIEND); + } else { + // MINT CASE + evm.prank(USER); + pool.mint(sharesToReachLimit, FRIEND); + } } } } - // [P4-8]: deposit adds liquidity correctly - function test_P4_08_mint_adds_liquidity_correctly() public { - // adds liqudity to mint initial diesel tokens to change 1:1 rate - - for (uint256 i; i < 2; ++i) { - bool feeToken = i == 1; - - _setUp(feeToken ? Tokens.USDT : Tokens.DAI); - - if (feeToken) { - // set 50% fee if fee token - ERC20FeeMock(pool.asset()).setMaximumFee(type(uint256).max); - ERC20FeeMock(pool.asset()).setBasisPointsRate(fee); - } - - _initPoolLiquidity(); - - uint256 desiredShares = addLiquidity / 2; - uint256 expectedAssetsPaid = feeToken ? _divFee(addLiquidity, fee) : addLiquidity; - uint256 expectedAvailableLiquidity = pool.availableLiquidity() + addLiquidity; + // + // WITHDRAW + // + struct WithdrawTestCase { + string name; + /// SETUP + Tokens asset; + uint256 tokenFee; + uint256 initialLiquidity; + uint256 dieselRate; + uint16 utilisation; + uint16 withdrawFee; + /// PARAMS + uint256 sharesToMint; + uint256 assetsToWithdraw; + /// EXPECTED VALUES + uint256 expectedSharesBurnt; + uint256 expectedAvailableLiquidity; + uint256 expectedLiquidityAfter; + uint256 expectedTreasury; + } - evm.expectEmit(true, true, false, true); - emit Transfer(address(0), FRIEND, desiredShares); + // [P4-8]: deposit and mint if assets more than limit + function test_P4_08_withdraw_works_as_expected() public { + WithdrawTestCase[4] memory cases = [ + WithdrawTestCase({ + name: "Normal token with 0 withdraw fee", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + sharesToMint: addLiquidity / 2, + assetsToWithdraw: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedSharesBurnt: addLiquidity / 8, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 4, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 4, + expectedTreasury: 0 + }), + WithdrawTestCase({ + name: "Normal token with 1% withdraw fee", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 1_00, + // PARAMS + sharesToMint: addLiquidity / 2, + assetsToWithdraw: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedSharesBurnt: addLiquidity / 8 * 100 / 99, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 4 * 100 / 99, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 4 * 100 / 99, + expectedTreasury: addLiquidity / 4 * 1 / 99 + }), + WithdrawTestCase({ + name: "Fee token with 0 withdraw fee", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + sharesToMint: addLiquidity / 2, + assetsToWithdraw: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedSharesBurnt: addLiquidity / 8 * 100 / 40, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 4 * 100 / 40, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 4 * 100 / 40, + expectedTreasury: 0 + }), + WithdrawTestCase({ + name: "Fee token with 1% withdraw fee", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 1_00, + // PARAMS + sharesToMint: addLiquidity / 2, + assetsToWithdraw: addLiquidity / 4, + // EXPECTED VALUES: + // + // addLiquidity /2 * 1/2 (rate) * 1 / (100%-1%) / feeToken + expectedSharesBurnt: addLiquidity / 8 * 100 / 99 * 100 / 40 + 1, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 4 * 100 / 40 * 100 / 99 - 1, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 4 * 100 / 40 * 100 / 99 - 1, + expectedTreasury: addLiquidity / 4 * 1 / 99 + 1 + }) + ]; + + for (uint256 i; i < cases.length; ++i) { + WithdrawTestCase memory testCase = cases[i]; + /// @dev a represents allowance, 0 means required amount +1, 1 means inlimited allowance + for (uint256 approveCase; approveCase < 2; ++approveCase) { + _setUpTestCase( + testCase.asset, + testCase.tokenFee, + testCase.utilisation, + testCase.initialLiquidity, + testCase.dieselRate, + testCase.withdrawFee + ); - evm.expectEmit(true, true, false, true); - emit Deposit(USER, FRIEND, expectedAssetsPaid, desiredShares); + evm.prank(USER); + pool.mint(testCase.sharesToMint, FRIEND); - uint256 gl = gasleft(); + evm.prank(FRIEND); + pool.approve(USER, approveCase == 0 ? testCase.expectedSharesBurnt + 1 : type(uint256).max); - evm.prank(USER); - uint256 assets = pool.mint(desiredShares, FRIEND); + evm.expectEmit(true, true, false, true); + emit Transfer(FRIEND, address(0), testCase.expectedSharesBurnt); - console.log(gl - gasleft()); + evm.expectEmit(true, true, false, true); + emit Withdraw(USER, FRIEND2, FRIEND, testCase.assetsToWithdraw, testCase.expectedSharesBurnt); - expectBalance(address(pool), FRIEND, desiredShares, "Incorrect shares "); - expectBalance(underlying, USER, liquidityProviderInitBalance - expectedAssetsPaid, "Incorrect USER balance"); - assertEq(pool.expectedLiquidity(), addLiquidity * 3, "Incorrect expected liquidity"); - assertEq(pool.availableLiquidity(), expectedAvailableLiquidity, "Incorrect available liquidity"); - assertEq(assets, expectedAssetsPaid, "Incorrect assets return value"); + evm.prank(USER); + uint256 shares = pool.withdraw(testCase.assetsToWithdraw, FRIEND2, FRIEND); + + expectBalance( + underlying, + FRIEND2, + testCase.assetsToWithdraw, + _testCaseErr(testCase.name, "Incorrect assets on FRIEND2 account") + ); + + expectBalance( + underlying, + pool.treasury(), + testCase.expectedTreasury, + _testCaseErr(testCase.name, "Incorrect DAO fee") + ); + assertEq( + shares, testCase.expectedSharesBurnt, _testCaseErr(testCase.name, "Incorrect shares return value") + ); + + expectBalance( + address(pool), + FRIEND, + testCase.sharesToMint - testCase.expectedSharesBurnt, + _testCaseErr(testCase.name, "Incorrect FRIEND balance") + ); + + assertEq( + pool.expectedLiquidity(), + testCase.expectedLiquidityAfter, + _testCaseErr(testCase.name, "Incorrect expected liquidity") + ); + assertEq( + pool.availableLiquidity(), + testCase.expectedAvailableLiquidity, + _testCaseErr(testCase.name, "Incorrect available liquidity") + ); + + assertEq( + pool.allowance(FRIEND, USER), + approveCase == 0 ? 1 : type(uint256).max, + _testCaseErr(testCase.name, "Incorrect allowance after operation") + ); + + assertEq( + pool.borrowRate(), + irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), + _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") + ); + } } } // - // REMOVE LIQUIDITY + // REDEEM // + struct RedeemTestCase { + string name; + /// SETUP + Tokens asset; + uint256 tokenFee; + uint256 initialLiquidity; + uint256 dieselRate; + uint16 utilisation; + uint16 withdrawFee; + /// PARAMS + uint256 sharesToMint; + uint256 sharesToRedeem; + /// EXPECTED VALUES + uint256 expectedAssetsDelivered; + uint256 expectedAvailableLiquidity; + uint256 expectedLiquidityAfter; + uint256 expectedTreasury; + } - // // [P4-5]: removeLiquidity correctly removes liquidity - // function test_PX_05_remove_liquidity_removes_correctly() public { - // evm.prank(USER); - // pool.depositReferral(addLiquidity, FRIEND, referral); - - // // evm.expectEmit(true, true, false, true); - // // emit RemoveLiquidity(FRIEND, USER, removeLiquidity); - - // evm.prank(FRIEND); - // pool.redeem(removeLiquidity, USER, FRIEND); + // [P4-9]: deposit and mint if assets more than limit + function test_P4_09_redeem_works_as_expected() public { + RedeemTestCase[4] memory cases = [ + RedeemTestCase({ + name: "Normal token with 0 withdraw fee", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + sharesToMint: addLiquidity / 2, + sharesToRedeem: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedAssetsDelivered: addLiquidity / 2, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, + expectedTreasury: 0 + }), + RedeemTestCase({ + name: "Normal token with 1% withdraw fee", + // POOL SETUP + asset: Tokens.DAI, + tokenFee: 0, + initialLiquidity: addLiquidity, + // 1 dDAI = 2 DAI + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 1_00, + // PARAMS + sharesToMint: addLiquidity / 2, + sharesToRedeem: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedAssetsDelivered: addLiquidity / 2 * 99 / 100, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, + expectedTreasury: addLiquidity / 2 * 1 / 100 + }), + RedeemTestCase({ + name: "Fee token with 0 withdraw fee", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 0, + // PARAMS + sharesToMint: addLiquidity / 2, + sharesToRedeem: addLiquidity / 4, + // EXPECTED VALUES: + // + // Depends on dieselRate + expectedAssetsDelivered: addLiquidity / 2 * 40 / 100, + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, + expectedTreasury: 0 + }), + RedeemTestCase({ + name: "Fee token with 1% withdraw fee", + /// SETUP + asset: Tokens.USDT, + // transfer fee: 60%, so 40% will be transfer to account + tokenFee: 60_00, + initialLiquidity: addLiquidity, + // 1 dUSDT = 2 USDT + dieselRate: 2 * RAY, + // 50% of available liquidity is borrowed + utilisation: 50_00, + withdrawFee: 1_00, + // PARAMS + sharesToMint: addLiquidity / 2, + sharesToRedeem: addLiquidity / 4, + // EXPECTED VALUES: + // + // addLiquidity /2 * 1/2 (rate) * 1 / (100%-1%) / feeToken + expectedAssetsDelivered: addLiquidity / 2 * 99 / 100 * 40 / 100, + // availableLiquidityBefore: addLiqudity /2 (cause 50% utilisation) + expectedAvailableLiquidity: addLiquidity / 2 + addLiquidity - addLiquidity / 2, + expectedLiquidityAfter: addLiquidity * 2 - addLiquidity / 2, + expectedTreasury: addLiquidity / 2 * 40 / 100 * 1 / 100 + }) + ]; + /// @dev a represents allowance, 0 means required amount +1, 1 means inlimited allowance + + for (uint256 i; i < cases.length; ++i) { + RedeemTestCase memory testCase = cases[i]; + for (uint256 approveCase; approveCase < 2; ++approveCase) { + bool feeToken = i == 1; + + _setUpTestCase( + testCase.asset, + testCase.tokenFee, + testCase.utilisation, + testCase.initialLiquidity, + testCase.dieselRate, + testCase.withdrawFee + ); - // expectBalance(address(pool), FRIEND, addLiquidity - removeLiquidity); - // expectBalance(underlying, USER, liquidityProviderInitBalance - addLiquidity + removeLiquidity); - // assertEq(pool.expectedLiquidity(), addLiquidity - removeLiquidity); - // assertEq(pool.availableLiquidity(), addLiquidity - removeLiquidity); - // } + evm.prank(USER); + pool.mint(testCase.sharesToMint, FRIEND); - // // [P4-7]: constructor set correct cumulative index to 1 at start - // function test_PX_07_starting_cumulative_index_correct() public { - // assertEq(pool.cumulativeIndexLU_RAY(), RAY); - // } + evm.prank(FRIEND); + pool.approve(USER, approveCase == 0 ? testCase.sharesToRedeem + 1 : type(uint256).max); - // // [P4-8]: getDieselRate_RAY correctly computes rate - // function test_PX_08_diesel_rate_computes_correctly() public { - // evm.prank(USER); - // pool.deposit(addLiquidity, FRIEND); + evm.expectEmit(true, true, false, true); + emit Transfer(FRIEND, address(0), testCase.sharesToRedeem); - // // pool.setExpectedLiquidityLU(addLiquidity * 2); + evm.expectEmit(true, true, false, true); + emit Withdraw(USER, FRIEND2, FRIEND, testCase.expectedAssetsDelivered, testCase.sharesToRedeem); - // assertEq(pool.expectedLiquidity(), addLiquidity * 2); - // assertEq(pool.getDieselRate_RAY(), RAY * 2); - // } + evm.prank(USER); + uint256 assets = pool.redeem(testCase.sharesToRedeem, FRIEND2, FRIEND); + + expectBalance( + underlying, + FRIEND2, + testCase.expectedAssetsDelivered, + _testCaseErr(testCase.name, "Incorrect assets on FRIEND2 account ") + ); + + expectBalance( + underlying, + pool.treasury(), + testCase.expectedTreasury, + _testCaseErr(testCase.name, "Incorrect treasury fee") + ); + assertEq( + assets, + testCase.expectedAssetsDelivered, + _testCaseErr(testCase.name, "Incorrect assets return value") + ); + expectBalance( + address(pool), + FRIEND, + testCase.sharesToMint - testCase.sharesToRedeem, + _testCaseErr(testCase.name, "Incorrect FRIEND balance") + ); + + assertEq( + pool.expectedLiquidity(), + testCase.expectedLiquidityAfter, + _testCaseErr(testCase.name, "Incorrect expected liquidity") + ); + assertEq( + pool.availableLiquidity(), + testCase.expectedAvailableLiquidity, + _testCaseErr(testCase.name, "Incorrect available liquidity") + ); + + assertEq( + pool.allowance(FRIEND, USER), + approveCase == 0 ? 1 : type(uint256).max, + _testCaseErr(testCase.name, "Incorrect allowance after operation") + ); + + assertEq( + pool.borrowRate(), + irm.calcBorrowRate(pool.expectedLiquidity(), pool.availableLiquidity(), false), + _testCaseErr(testCase.name, "Borrow rate wasn't update correcty") + ); + } + } + } - // // [P4-9]: addLiquidity correctly adds liquidity with DieselRate != 1 - // function test_PX_09_correctly_adds_liquidity_at_new_diesel_rate() public { - // evm.prank(USER); - // pool.deposit(addLiquidity, USER); + // [P4-10]: burn works as expected + function test_P4_10_burn_works_as_expected() public { + _setUpTestCase(Tokens.DAI, 0, 50_00, addLiquidity, 2 * RAY, 0); - // // pool.setExpectedLiquidityLU(addLiquidity * 2); + evm.prank(USER); + pool.mint(addLiquidity, USER); - // evm.prank(USER); - // pool.deposit(addLiquidity, FRIEND); + uint256 borrowRate = pool.borrowRate(); + uint256 dieselRate = pool.convertToAssets(RAY); + uint256 availableLiquidity = pool.availableLiquidity(); + uint256 expectedLiquidity = pool.expectedLiquidity(); - // assertEq(pool.balanceOf(FRIEND), addLiquidity / 2); - // } + expectBalance(address(pool), USER, addLiquidity, "Incorrect USER balance"); - // // [P4-10]: removeLiquidity correctly removes liquidity if diesel rate != 1 - // function test_PX_10_correctly_removes_liquidity_at_new_diesel_rate() public { - // evm.prank(USER); - // pool.deposit(addLiquidity, FRIEND); + /// Initial lp provided 1/2 AL + 1AL from USER + assertEq(pool.totalSupply(), addLiquidity * 3 / 2, "Incorrect total supply"); - // // pool.setExpectedLiquidityLU(uint128(addLiquidity * 2)); + evm.prank(USER); + pool.burn(addLiquidity / 4); - // evm.prank(FRIEND); - // pool.redeem(removeLiquidity, USER, FRIEND); + expectBalance(address(pool), USER, addLiquidity * 3 / 4, "Incorrect USER balance"); - // expectBalance(address(pool), FRIEND, addLiquidity - removeLiquidity); - // expectBalance(underlying, USER, liquidityProviderInitBalance - addLiquidity + 2 * removeLiquidity); - // assertEq(pool.expectedLiquidity(), (addLiquidity - removeLiquidity) * 2); - // assertEq(pool.availableLiquidity(), addLiquidity - removeLiquidity * 2); - // } + assertEq(pool.borrowRate(), borrowRate, "Incorrect borrow rate"); + /// Before burn totalSupply was 150% * AL, after 125% * LP + assertEq(pool.convertToAssets(RAY), dieselRate * 150 / 125, "Incorrect diesel rate"); + assertEq(pool.availableLiquidity(), availableLiquidity, "Incorrect borrow rate"); + assertEq(pool.expectedLiquidity(), expectedLiquidity, "Incorrect borrow rate"); + } // // [P4-11]: connectCreditManager, forbidCreditManagerToBorrow, newInterestRateModel, setExpecetedLiquidityLimit reverts if called with non-configurator // function test_PX_11_admin_functions_revert_on_non_admin() public { @@ -599,7 +1106,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // uint256 expectedBorrowRate = psts.linearIRModel().calcBorrowRate(expectedLiquidity, expectedAvailable); - // assertEq(expectedBorrowRate, pool.borrowRate_RAY(), "Borrow rate is incorrect"); + // assertEq(expectedBorrowRate, pool.borrowRate(), "Borrow rate is incorrect"); // } // // [P4-18]: repayCreditAccount emits Repay event @@ -635,7 +1142,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // cmMock.lendCreditAccount(addLiquidity / 2, ca); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // uint256 expectedInterest = ((addLiquidity / 2) * borrowRate) / RAY; @@ -658,7 +1165,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // assertEq(pool.balanceOf(treasury), 0, "dToken remains in the treasury"); - // assertEq(pool.borrowRate_RAY(), expectedBorrowRate, "Borrow rate was not updated correctly"); + // assertEq(pool.borrowRate(), expectedBorrowRate, "Borrow rate was not updated correctly"); // } // // [P4-20]: repayCreditAccount correctly updates params on loss accrued: treasury >= loss; and emits event @@ -681,7 +1188,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // uint256 treasuryUnderlying = pool.convertToAssets(pool.balanceOf(treasury)); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // evm.warp(block.timestamp + timeWarp); @@ -699,7 +1206,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // assertEq(pool.balanceOf(treasury), expectedTreasury, "dToken balance incorrect"); - // assertEq(pool.borrowRate_RAY(), expectedBorrowRate, "Borrow rate was not updated correctly"); + // assertEq(pool.borrowRate(), expectedBorrowRate, "Borrow rate was not updated correctly"); // } // // [P4-21]: repayCreditAccount correctly updates params on profit @@ -714,7 +1221,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // cmMock.lendCreditAccount(addLiquidity / 2, ca); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // evm.warp(block.timestamp + timeWarp); @@ -735,7 +1242,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // assertEq(pool.balanceOf(treasury), pool.convertToShares(100), "dToken balance incorrect"); - // assertEq(pool.borrowRate_RAY(), expectedBorrowRate, "Borrow rate was not updated correctly"); + // assertEq(pool.borrowRate(), expectedBorrowRate, "Borrow rate was not updated correctly"); // } // // [P4-22]: repayCreditAccount does not change the diesel rate outside margin of error @@ -749,7 +1256,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // cmMock.lendCreditAccount(addLiquidity / 2, ca); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // evm.warp(block.timestamp + timeWarp); @@ -844,7 +1351,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // assertEq( // newIR.calcBorrowRate(expectedLiquidity, availableLiquidity), - // pool.borrowRate_RAY(), + // pool.borrowRate(), // "Borrow rate does not match" // ); // } @@ -860,7 +1367,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // cmMock.lendCreditAccount(addLiquidity / 2, ca); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // evm.warp(block.timestamp + timeWarp); @@ -876,7 +1383,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // assertEq(uint256(pool.timestampLU()), block.timestamp, "Timestamp was not updated correctly"); - // assertEq(pool.borrowRate_RAY(), expectedBorrowRate, "Borrow rate was not updated correctly"); + // assertEq(pool.borrowRate(), expectedBorrowRate, "Borrow rate was not updated correctly"); // assertEq(pool.calcLinearCumulative_RAY(), pool.cumulativeIndexLU_RAY(), "Index value was not updated correctly"); // } @@ -896,7 +1403,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // evm.warp(block.timestamp + timeWarp); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 expectedLinearRate = RAY + (borrowRate * timeWarp) / 365 days; @@ -914,7 +1421,7 @@ contract Pool4626Test is DSTest, BalanceHelper, IPool4626Events, IERC4626Events // cmMock.lendCreditAccount(addLiquidity / 2, ca); - // uint256 borrowRate = pool.borrowRate_RAY(); + // uint256 borrowRate = pool.borrowRate(); // uint256 timeWarp = 365 days; // evm.warp(block.timestamp + timeWarp); diff --git a/contracts/test/pool/PoolService.t.sol b/contracts/test/pool/PoolService.t.sol index 94601f8..a7e334f 100644 --- a/contracts/test/pool/PoolService.t.sol +++ b/contracts/test/pool/PoolService.t.sol @@ -50,6 +50,7 @@ contract PoolServiceTest is DSTest, BalanceHelper, IPoolServiceEvents { psts = new PoolServiceTestSuite( tokenTestSuite, tokenTestSuite.addressOf(Tokens.DAI), + false, false ); diff --git a/contracts/test/suites/PoolServiceTestSuite.sol b/contracts/test/suites/PoolServiceTestSuite.sol index 1dfbad9..afc071c 100644 --- a/contracts/test/suites/PoolServiceTestSuite.sol +++ b/contracts/test/suites/PoolServiceTestSuite.sol @@ -22,6 +22,7 @@ import {ERC20FeeMock} from "../mocks/token/ERC20FeeMock.sol"; import "../lib/constants.sol"; import {ITokenTestSuite} from "../interfaces/ITokenTestSuite.sol"; import {Pool4626} from "../../pool/Pool4626.sol"; +import {PoolQuotaKeeper} from "../../pool/PoolQuotaKeeper.sol"; import {Pool4626_USDT} from "../../pool/Pool4626_USDT.sol"; @@ -45,10 +46,11 @@ contract PoolServiceTestSuite { IERC20 public underlying; DieselToken public dieselToken; LinearInterestRateModel public linearIRModel; + PoolQuotaKeeper public poolQuotaKeeper; address public treasury; - constructor(ITokenTestSuite _tokenTestSuite, address _underlying, bool is4626) { + constructor(ITokenTestSuite _tokenTestSuite, address _underlying, bool is4626, bool supportQuotas) { linearIRModel = new LinearInterestRateModel( 8000, 9000, @@ -90,10 +92,14 @@ contract PoolServiceTestSuite { underlyingToken: _underlying, interestRateModel: address(linearIRModel), expectedLiquidityLimit: type(uint256).max, - supportsQuotas: false + supportsQuotas: supportQuotas }); pool4626 = isFeeToken ? new Pool4626_USDT(opts) : new Pool4626(opts); newPool = address(pool4626); + + if (supportQuotas) { + _deployAndConnectPoolQuotaKeeper(); + } } else { poolService = new TestPoolService( address(addressProvider), @@ -129,4 +135,11 @@ contract PoolServiceTestSuite { evm.stopPrank(); } + + function _deployAndConnectPoolQuotaKeeper() internal { + poolQuotaKeeper = new PoolQuotaKeeper(address(pool4626)); + + evm.prank(CONFIGURATOR); + pool4626.connectPoolQuotaManager(address(poolQuotaKeeper)); + } } diff --git a/contracts/test/suites/TokensTestSuiteHelper.sol b/contracts/test/suites/TokensTestSuiteHelper.sol index 045cdfc..d15afb3 100644 --- a/contracts/test/suites/TokensTestSuiteHelper.sol +++ b/contracts/test/suites/TokensTestSuiteHelper.sol @@ -30,10 +30,10 @@ contract TokensTestSuiteHelper is DSTest, ITokenTestSuite { if (token == wethToken) { evm.deal(address(this), amount); IWETH(wethToken).deposit{value: amount}(); + IERC20(token).transfer(to, amount); } else { - ERC20Mock(token).mint(address(this), amount); + ERC20Mock(token).mint(to, amount); } - IERC20(token).transfer(to, amount); } function balanceOf(address token, address holder) public view override returns (uint256 balance) {