diff --git a/.env.example b/.env.example index e3c1ce36a..dc896fbff 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,6 @@ ETHERSCAN_API_KEY= # Set to true or false to control gas reporting REPORT_GAS= + +# Set to true or false to deploy contracts based on timestamp or block +IS_TIME_BASED_DEPLOYMENT=false \ No newline at end of file diff --git a/README.md b/README.md index 9c7160504..48cc9cc89 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ REPORT_GAS=true npx hardhat test ``` -- To run fork tests add FORK_MAINNET=true and QUICK_NODE_KEY in the .env file. +- To run fork tests add FORK=true, FORKED_NETWORK and one ARCHIVE_NODE var in the .env file. ## Deployment diff --git a/audits/088_timeBased_certik_20240117.pdf b/audits/088_timeBased_certik_20240117.pdf new file mode 100644 index 000000000..2edc369c3 Binary files /dev/null and b/audits/088_timeBased_certik_20240117.pdf differ diff --git a/audits/089_timeBased_quantstamp_20240319.pdf b/audits/089_timeBased_quantstamp_20240319.pdf new file mode 100644 index 000000000..82b772644 Binary files /dev/null and b/audits/089_timeBased_quantstamp_20240319.pdf differ diff --git a/audits/094_timeBased_fairyproof_20240304.pdf b/audits/094_timeBased_fairyproof_20240304.pdf new file mode 100644 index 000000000..ed44b7dba Binary files /dev/null and b/audits/094_timeBased_fairyproof_20240304.pdf differ diff --git a/contracts/BaseJumpRateModelV2.sol b/contracts/BaseJumpRateModelV2.sol deleted file mode 100644 index 2def8bfe5..000000000 --- a/contracts/BaseJumpRateModelV2.sol +++ /dev/null @@ -1,207 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -pragma solidity 0.8.25; - -import { IAccessControlManagerV8 } from "@venusprotocol/governance-contracts/contracts/Governance/IAccessControlManagerV8.sol"; - -import { InterestRateModel } from "./InterestRateModel.sol"; -import { EXP_SCALE, MANTISSA_ONE } from "./lib/constants.sol"; - -/** - * @title Logic for Compound's JumpRateModel Contract V2. - * @author Compound (modified by Dharma Labs, Arr00 and Venus) - * @notice An interest rate model with a steep increase after a certain utilization threshold called **kink** is reached. - * The parameters of this interest rate model can be adjusted by the owner. Version 2 modifies Version 1 by enabling updateable parameters. - */ -abstract contract BaseJumpRateModelV2 is InterestRateModel { - /** - * @notice The approximate number of blocks per year that is assumed by the interest rate model - */ - uint256 public immutable blocksPerYear; - /** - * @notice The address of the AccessControlManager contract - */ - IAccessControlManagerV8 public accessControlManager; - - /** - * @notice The multiplier of utilization rate that gives the slope of the interest rate - */ - uint256 public multiplierPerBlock; - - /** - * @notice The base interest rate which is the y-intercept when utilization rate is 0 - */ - uint256 public baseRatePerBlock; - - /** - * @notice The multiplier per block after hitting a specified utilization point - */ - uint256 public jumpMultiplierPerBlock; - - /** - * @notice The utilization point at which the jump multiplier is applied - */ - uint256 public kink; - - event NewInterestParams( - uint256 baseRatePerBlock, - uint256 multiplierPerBlock, - uint256 jumpMultiplierPerBlock, - uint256 kink - ); - - /** - * @notice Thrown when the action is prohibited by AccessControlManager - */ - error Unauthorized(address sender, address calledContract, string methodSignature); - - /** - * @notice Construct an interest rate model - * @param blocksPerYear_ The approximate number of blocks per year that is assumed by the interest rate model. - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) - * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) - * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point - * @param kink_ The utilization point at which the jump multiplier is applied - * @param accessControlManager_ The address of the AccessControlManager contract - */ - constructor( - uint256 blocksPerYear_, - uint256 baseRatePerYear, - uint256 multiplierPerYear, - uint256 jumpMultiplierPerYear, - uint256 kink_, - IAccessControlManagerV8 accessControlManager_ - ) { - require(address(accessControlManager_) != address(0), "invalid ACM address"); - require(blocksPerYear_ != 0, "Invalid blocks per year"); - accessControlManager = accessControlManager_; - blocksPerYear = blocksPerYear_; - - _updateJumpRateModel(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_); - } - - /** - * @notice Update the parameters of the interest rate model - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) - * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) - * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point - * @param kink_ The utilization point at which the jump multiplier is applied - * @custom:error Unauthorized if the sender is not allowed to call this function - * @custom:access Controlled by AccessControlManager - */ - function updateJumpRateModel( - uint256 baseRatePerYear, - uint256 multiplierPerYear, - uint256 jumpMultiplierPerYear, - uint256 kink_ - ) external virtual { - string memory signature = "updateJumpRateModel(uint256,uint256,uint256,uint256)"; - bool isAllowedToCall = accessControlManager.isAllowedToCall(msg.sender, signature); - - if (!isAllowedToCall) { - revert Unauthorized(msg.sender, address(this), signature); - } - - _updateJumpRateModel(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_); - } - - /** - * @notice Calculates the current supply rate per block - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market - * @param reserveFactorMantissa The current reserve factor for the market - * @param badDebt The amount of badDebt in the market - * @return The supply rate percentage per block as a mantissa (scaled by EXP_SCALE) - */ - function getSupplyRate( - uint256 cash, - uint256 borrows, - uint256 reserves, - uint256 reserveFactorMantissa, - uint256 badDebt - ) public view virtual override returns (uint256) { - uint256 oneMinusReserveFactor = MANTISSA_ONE - reserveFactorMantissa; - uint256 borrowRate = _getBorrowRate(cash, borrows, reserves, badDebt); - uint256 rateToPool = (borrowRate * oneMinusReserveFactor) / EXP_SCALE; - uint256 incomeToDistribute = borrows * rateToPool; - uint256 supply = cash + borrows + badDebt - reserves; - return incomeToDistribute / supply; - } - - /** - * @notice Calculates the utilization rate of the market: `(borrows + badDebt) / (cash + borrows + badDebt - reserves)` - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market (currently unused) - * @param badDebt The amount of badDebt in the market - * @return The utilization rate as a mantissa between [0, MANTISSA_ONE] - */ - function utilizationRate( - uint256 cash, - uint256 borrows, - uint256 reserves, - uint256 badDebt - ) public pure returns (uint256) { - // Utilization rate is 0 when there are no borrows and badDebt - if ((borrows + badDebt) == 0) { - return 0; - } - - uint256 rate = ((borrows + badDebt) * EXP_SCALE) / (cash + borrows + badDebt - reserves); - - if (rate > EXP_SCALE) { - rate = EXP_SCALE; - } - - return rate; - } - - /** - * @notice Internal function to update the parameters of the interest rate model - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) - * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) - * @param jumpMultiplierPerYear The multiplierPerBlock after hitting a specified utilization point - * @param kink_ The utilization point at which the jump multiplier is applied - */ - function _updateJumpRateModel( - uint256 baseRatePerYear, - uint256 multiplierPerYear, - uint256 jumpMultiplierPerYear, - uint256 kink_ - ) internal { - baseRatePerBlock = baseRatePerYear / blocksPerYear; - multiplierPerBlock = multiplierPerYear / blocksPerYear; - jumpMultiplierPerBlock = jumpMultiplierPerYear / blocksPerYear; - kink = kink_; - - emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink); - } - - /** - * @notice Calculates the current borrow rate per block, with the error code expected by the market - * @param cash The amount of cash in the market - * @param borrows The amount of borrows in the market - * @param reserves The amount of reserves in the market - * @param badDebt The amount of badDebt in the market - * @return The borrow rate percentage per block as a mantissa (scaled by EXP_SCALE) - */ - function _getBorrowRate( - uint256 cash, - uint256 borrows, - uint256 reserves, - uint256 badDebt - ) internal view returns (uint256) { - uint256 util = utilizationRate(cash, borrows, reserves, badDebt); - uint256 kink_ = kink; - - if (util <= kink_) { - return ((util * multiplierPerBlock) / EXP_SCALE) + baseRatePerBlock; - } - uint256 normalRate = ((kink_ * multiplierPerBlock) / EXP_SCALE) + baseRatePerBlock; - uint256 excessUtil; - unchecked { - excessUtil = util - kink_; - } - return ((excessUtil * jumpMultiplierPerBlock) / EXP_SCALE) + normalRate; - } -} diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 77cb9eba3..3a50f68a8 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -1091,7 +1091,7 @@ contract Comptroller is /** * @notice Add a new RewardsDistributor and initialize it with all markets. We can add several RewardsDistributor - * contracts with the same rewardToken, and there could be overlaping among them considering the last reward block + * contracts with the same rewardToken, and there could be overlaping among them considering the last reward slot (block or second) * @dev Only callable by the admin * @param _rewardsDistributor Address of the RewardDistributor contract to add * @custom:access Only Governance diff --git a/contracts/InterestRateModel.sol b/contracts/InterestRateModel.sol index 052068c23..ae49218cf 100644 --- a/contracts/InterestRateModel.sol +++ b/contracts/InterestRateModel.sol @@ -7,12 +7,12 @@ pragma solidity 0.8.25; */ abstract contract InterestRateModel { /** - * @notice Calculates the current borrow interest rate per block + * @notice Calculates the current borrow interest rate per slot (block or second) * @param cash The total amount of cash the market has * @param borrows The total amount of borrows the market has outstanding * @param reserves The total amount of reserves the market has * @param badDebt The amount of badDebt in the market - * @return The borrow rate per block (as a percentage, and scaled by 1e18) + * @return The borrow rate percentage per slot (block or second) as a mantissa (scaled by EXP_SCALE) */ function getBorrowRate( uint256 cash, @@ -22,13 +22,13 @@ abstract contract InterestRateModel { ) external view virtual returns (uint256); /** - * @notice Calculates the current supply interest rate per block + * @notice Calculates the current supply interest rate per slot (block or second) * @param cash The total amount of cash the market has * @param borrows The total amount of borrows the market has outstanding * @param reserves The total amount of reserves the market has * @param reserveFactorMantissa The current reserve factor the market has * @param badDebt The amount of badDebt in the market - * @return The supply rate per block (as a percentage, and scaled by 1e18) + * @return The supply rate percentage per slot (block or second) as a mantissa (scaled by EXP_SCALE) */ function getSupplyRate( uint256 cash, diff --git a/contracts/JumpRateModelV2.sol b/contracts/JumpRateModelV2.sol index 0f670e0fa..541d8144c 100644 --- a/contracts/JumpRateModelV2.sol +++ b/contracts/JumpRateModelV2.sol @@ -2,43 +2,112 @@ pragma solidity 0.8.25; import { IAccessControlManagerV8 } from "@venusprotocol/governance-contracts/contracts/Governance/IAccessControlManagerV8.sol"; - -import { BaseJumpRateModelV2 } from "./BaseJumpRateModelV2.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; +import { InterestRateModel } from "./InterestRateModel.sol"; +import { EXP_SCALE, MANTISSA_ONE } from "./lib/constants.sol"; /** - * @title Compound's JumpRateModel Contract V2 for V2 vTokens - * @author Arr00 - * @notice Supports only for V2 vTokens + * @title JumpRateModelV2 + * @author Compound (modified by Dharma Labs, Arr00 and Venus) + * @notice An interest rate model with a steep increase after a certain utilization threshold called **kink** is reached. + * The parameters of this interest rate model can be adjusted by the owner. Version 2 modifies Version 1 by enabling updateable parameters */ -contract JumpRateModelV2 is BaseJumpRateModelV2 { +contract JumpRateModelV2 is InterestRateModel, TimeManagerV8 { + /** + * @notice The address of the AccessControlManager contract + */ + IAccessControlManagerV8 public accessControlManager; + + /** + * @notice The multiplier of utilization rate per block or second that gives the slope of the interest rate + */ + uint256 public multiplierPerBlock; + + /** + * @notice The base interest rate per block or second which is the y-intercept when utilization rate is 0 + */ + uint256 public baseRatePerBlock; + + /** + * @notice The multiplier per block or second after hitting a specified utilization point + */ + uint256 public jumpMultiplierPerBlock; + + /** + * @notice The utilization point at which the jump multiplier is applied + */ + uint256 public kink; + + event NewInterestParams( + uint256 baseRatePerBlockOrTimestamp, + uint256 multiplierPerBlockOrTimestamp, + uint256 jumpMultiplierPerBlockOrTimestamp, + uint256 kink + ); + + /** + * @notice Thrown when the action is prohibited by AccessControlManager + */ + error Unauthorized(address sender, address calledContract, string methodSignature); + + /** + * @notice Construct an interest rate model + * @param baseRatePerYear_ The approximate target base APR, as a mantissa (scaled by EXP_SCALE) + * @param multiplierPerYear_ The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) + * @param jumpMultiplierPerYear_ The multiplier after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + * @param accessControlManager_ The address of the AccessControlManager contract + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + */ constructor( - uint256 blocksPerYear_, + uint256 baseRatePerYear_, + uint256 multiplierPerYear_, + uint256 jumpMultiplierPerYear_, + uint256 kink_, + IAccessControlManagerV8 accessControlManager_, + bool timeBased_, + uint256 blocksPerYear_ + ) TimeManagerV8(timeBased_, blocksPerYear_) { + require(address(accessControlManager_) != address(0), "invalid ACM address"); + + accessControlManager = accessControlManager_; + + _updateJumpRateModel(baseRatePerYear_, multiplierPerYear_, jumpMultiplierPerYear_, kink_); + } + + /** + * @notice Update the parameters of the interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) + * @param jumpMultiplierPerYear The multiplierPerBlockOrTimestamp after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + * @custom:error Unauthorized if the sender is not allowed to call this function + * @custom:access Controlled by AccessControlManager + */ + function updateJumpRateModel( uint256 baseRatePerYear, uint256 multiplierPerYear, uint256 jumpMultiplierPerYear, - uint256 kink_, - IAccessControlManagerV8 accessControlManager_ - ) - BaseJumpRateModelV2( - blocksPerYear_, - baseRatePerYear, - multiplierPerYear, - jumpMultiplierPerYear, - kink_, - accessControlManager_ - ) - /* solhint-disable-next-line no-empty-blocks */ - { + uint256 kink_ + ) external virtual { + string memory signature = "updateJumpRateModel(uint256,uint256,uint256,uint256)"; + bool isAllowedToCall = accessControlManager.isAllowedToCall(msg.sender, signature); + if (!isAllowedToCall) { + revert Unauthorized(msg.sender, address(this), signature); + } + + _updateJumpRateModel(baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_); } /** - * @notice Calculates the current borrow rate per block + * @notice Calculates the current borrow rate per slot (block or second) * @param cash The amount of cash in the market * @param borrows The amount of borrows in the market * @param reserves The amount of reserves in the market * @param badDebt The amount of badDebt in the market - * @return The borrow rate percentage per block as a mantissa (scaled by 1e18) + * @return The borrow rate percentage per slot (block or second) as a mantissa (scaled by 1e18) */ function getBorrowRate( uint256 cash, @@ -48,4 +117,105 @@ contract JumpRateModelV2 is BaseJumpRateModelV2 { ) external view override returns (uint256) { return _getBorrowRate(cash, borrows, reserves, badDebt); } + + /** + * @notice Calculates the current supply rate per slot (block or second) + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @param reserveFactorMantissa The current reserve factor for the market + * @param badDebt The amount of badDebt in the market + * @return The supply rate percentage per slot (block or second) as a mantissa (scaled by EXP_SCALE) + */ + function getSupplyRate( + uint256 cash, + uint256 borrows, + uint256 reserves, + uint256 reserveFactorMantissa, + uint256 badDebt + ) public view virtual override returns (uint256) { + uint256 oneMinusReserveFactor = MANTISSA_ONE - reserveFactorMantissa; + uint256 borrowRate = _getBorrowRate(cash, borrows, reserves, badDebt); + uint256 rateToPool = (borrowRate * oneMinusReserveFactor) / EXP_SCALE; + uint256 incomeToDistribute = borrows * rateToPool; + uint256 supply = cash + borrows + badDebt - reserves; + return incomeToDistribute / supply; + } + + /** + * @notice Calculates the utilization rate of the market: `(borrows + badDebt) / (cash + borrows + badDebt - reserves)` + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market (currently unused) + * @param badDebt The amount of badDebt in the market + * @return The utilization rate as a mantissa between [0, MANTISSA_ONE] + */ + function utilizationRate( + uint256 cash, + uint256 borrows, + uint256 reserves, + uint256 badDebt + ) public pure returns (uint256) { + // Utilization rate is 0 when there are no borrows and badDebt + if ((borrows + badDebt) == 0) { + return 0; + } + + uint256 rate = ((borrows + badDebt) * EXP_SCALE) / (cash + borrows + badDebt - reserves); + + if (rate > EXP_SCALE) { + rate = EXP_SCALE; + } + + return rate; + } + + /** + * @notice Internal function to update the parameters of the interest rate model + * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) + * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) + * @param jumpMultiplierPerYear The multiplierPerBlockOrTimestamp after hitting a specified utilization point + * @param kink_ The utilization point at which the jump multiplier is applied + */ + function _updateJumpRateModel( + uint256 baseRatePerYear, + uint256 multiplierPerYear, + uint256 jumpMultiplierPerYear, + uint256 kink_ + ) internal { + baseRatePerBlock = baseRatePerYear / blocksOrSecondsPerYear; + multiplierPerBlock = multiplierPerYear / blocksOrSecondsPerYear; + jumpMultiplierPerBlock = jumpMultiplierPerYear / blocksOrSecondsPerYear; + kink = kink_; + + emit NewInterestParams(baseRatePerBlock, multiplierPerBlock, jumpMultiplierPerBlock, kink); + } + + /** + * @notice Calculates the current borrow rate per slot (block or second), with the error code expected by the market + * @param cash The amount of cash in the market + * @param borrows The amount of borrows in the market + * @param reserves The amount of reserves in the market + * @param badDebt The amount of badDebt in the market + * @return The borrow rate percentage per slot (block or second) as a mantissa (scaled by EXP_SCALE) + */ + function _getBorrowRate( + uint256 cash, + uint256 borrows, + uint256 reserves, + uint256 badDebt + ) internal view returns (uint256) { + uint256 util = utilizationRate(cash, borrows, reserves, badDebt); + uint256 kink_ = kink; + + if (util <= kink_) { + return ((util * multiplierPerBlock) / EXP_SCALE) + baseRatePerBlock; + } + uint256 normalRate = ((kink_ * multiplierPerBlock) / EXP_SCALE) + baseRatePerBlock; + uint256 excessUtil; + unchecked { + excessUtil = util - kink_; + } + return ((excessUtil * jumpMultiplierPerBlock) / EXP_SCALE) + normalRate; + } } diff --git a/contracts/Lens/PoolLens.sol b/contracts/Lens/PoolLens.sol index 0b12e1b0e..47fe57ca5 100644 --- a/contracts/Lens/PoolLens.sol +++ b/contracts/Lens/PoolLens.sol @@ -11,6 +11,7 @@ import { Action, ComptrollerInterface, ComptrollerViewInterface } from "../Compt import { PoolRegistryInterface } from "../Pool/PoolRegistryInterface.sol"; import { PoolRegistry } from "../Pool/PoolRegistry.sol"; import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; /** * @title PoolLens @@ -25,7 +26,7 @@ import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol"; - the underlying asset price of a vToken; - the metadata (exchange/borrow/supply rate, total supply, collateral factor, etc) of any vToken. */ -contract PoolLens is ExponentialNoError { +contract PoolLens is ExponentialNoError, TimeManagerV8 { /** * @dev Struct for PoolDetails. */ @@ -51,8 +52,8 @@ contract PoolLens is ExponentialNoError { struct VTokenMetadata { address vToken; uint256 exchangeRateCurrent; - uint256 supplyRatePerBlock; - uint256 borrowRatePerBlock; + uint256 supplyRatePerBlockOrTimestamp; + uint256 borrowRatePerBlockOrTimestamp; uint256 reserveFactorMantissa; uint256 supplyCaps; uint256 borrowCaps; @@ -66,6 +67,7 @@ contract PoolLens is ExponentialNoError { uint256 vTokenDecimals; uint256 underlyingDecimals; uint256 pausedActions; + bool isTimeBased; } /** @@ -112,10 +114,10 @@ contract PoolLens is ExponentialNoError { struct RewardTokenState { // The market's last updated rewardTokenBorrowIndex or rewardTokenSupplyIndex uint224 index; - // The block number the index was last updated at - uint32 block; - // The block number at which to stop rewards - uint32 lastRewardingBlock; + // The block number or timestamp the index was last updated at + uint256 blockOrTimestamp; + // The block number or timestamp at which to stop rewards + uint256 lastRewardingBlockOrTimestamp; } /** @@ -135,6 +137,13 @@ contract PoolLens is ExponentialNoError { BadDebt[] badDebts; } + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bool timeBased_, uint256 blocksPerYear_) TimeManagerV8(timeBased_, blocksPerYear_) {} + /** * @notice Queries the user's supply/borrow balances in vTokens * @param vTokens The list of vToken addresses @@ -391,8 +400,8 @@ contract PoolLens is ExponentialNoError { VTokenMetadata({ vToken: address(vToken), exchangeRateCurrent: exchangeRateCurrent, - supplyRatePerBlock: vToken.supplyRatePerBlock(), - borrowRatePerBlock: vToken.borrowRatePerBlock(), + supplyRatePerBlockOrTimestamp: vToken.supplyRatePerBlock(), + borrowRatePerBlockOrTimestamp: vToken.borrowRatePerBlock(), reserveFactorMantissa: vToken.reserveFactorMantissa(), supplyCaps: comptroller.supplyCaps(address(vToken)), borrowCaps: comptroller.borrowCaps(address(vToken)), @@ -405,7 +414,8 @@ contract PoolLens is ExponentialNoError { underlyingAssetAddress: underlyingAssetAddress, vTokenDecimals: vToken.decimals(), underlyingDecimals: underlyingDecimals, - pausedActions: pausedActions + pausedActions: pausedActions, + isTimeBased: vToken.isTimeBased() }); } @@ -445,14 +455,39 @@ contract PoolLens is ExponentialNoError { RewardsDistributor rewardsDistributor ) internal view returns (PendingReward[] memory) { PendingReward[] memory pendingRewards = new PendingReward[](markets.length); + + bool _isTimeBased = rewardsDistributor.isTimeBased(); + require(_isTimeBased == isTimeBased, "Inconsistent Reward mode"); + for (uint256 i; i < markets.length; ++i) { // Market borrow and supply state we will modify update in-memory, in order to not modify storage RewardTokenState memory borrowState; - (borrowState.index, borrowState.block, borrowState.lastRewardingBlock) = rewardsDistributor - .rewardTokenBorrowState(address(markets[i])); RewardTokenState memory supplyState; - (supplyState.index, supplyState.block, supplyState.lastRewardingBlock) = rewardsDistributor - .rewardTokenSupplyState(address(markets[i])); + + if (_isTimeBased) { + ( + borrowState.index, + borrowState.blockOrTimestamp, + borrowState.lastRewardingBlockOrTimestamp + ) = rewardsDistributor.rewardTokenBorrowStateTimeBased(address(markets[i])); + ( + supplyState.index, + supplyState.blockOrTimestamp, + supplyState.lastRewardingBlockOrTimestamp + ) = rewardsDistributor.rewardTokenSupplyStateTimeBased(address(markets[i])); + } else { + ( + borrowState.index, + borrowState.blockOrTimestamp, + borrowState.lastRewardingBlockOrTimestamp + ) = rewardsDistributor.rewardTokenBorrowState(address(markets[i])); + ( + supplyState.index, + supplyState.blockOrTimestamp, + supplyState.lastRewardingBlockOrTimestamp + ) = rewardsDistributor.rewardTokenSupplyState(address(markets[i])); + } + Exp memory marketBorrowIndex = Exp({ mantissa: markets[i].borrowIndex() }); // Update market supply and borrow index in-memory @@ -489,23 +524,26 @@ contract PoolLens is ExponentialNoError { Exp memory marketBorrowIndex ) internal view { uint256 borrowSpeed = rewardsDistributor.rewardTokenBorrowSpeeds(vToken); - uint256 blockNumber = block.number; + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); - if (borrowState.lastRewardingBlock > 0 && blockNumber > borrowState.lastRewardingBlock) { - blockNumber = borrowState.lastRewardingBlock; + if ( + borrowState.lastRewardingBlockOrTimestamp > 0 && + blockNumberOrTimestamp > borrowState.lastRewardingBlockOrTimestamp + ) { + blockNumberOrTimestamp = borrowState.lastRewardingBlockOrTimestamp; } - uint256 deltaBlocks = sub_(blockNumber, uint256(borrowState.block)); - if (deltaBlocks > 0 && borrowSpeed > 0) { + uint256 deltaBlocksOrTimestamp = sub_(blockNumberOrTimestamp, borrowState.blockOrTimestamp); + if (deltaBlocksOrTimestamp > 0 && borrowSpeed > 0) { // Remove the total earned interest rate since the opening of the market from total borrows uint256 borrowAmount = div_(VToken(vToken).totalBorrows(), marketBorrowIndex); - uint256 tokensAccrued = mul_(deltaBlocks, borrowSpeed); + uint256 tokensAccrued = mul_(deltaBlocksOrTimestamp, borrowSpeed); Double memory ratio = borrowAmount > 0 ? fraction(tokensAccrued, borrowAmount) : Double({ mantissa: 0 }); Double memory index = add_(Double({ mantissa: borrowState.index }), ratio); borrowState.index = safe224(index.mantissa, "new index overflows"); - borrowState.block = safe32(blockNumber, "block number overflows"); - } else if (deltaBlocks > 0) { - borrowState.block = safe32(blockNumber, "block number overflows"); + borrowState.blockOrTimestamp = blockNumberOrTimestamp; + } else if (deltaBlocksOrTimestamp > 0) { + borrowState.blockOrTimestamp = blockNumberOrTimestamp; } } @@ -515,22 +553,25 @@ contract PoolLens is ExponentialNoError { RewardTokenState memory supplyState ) internal view { uint256 supplySpeed = rewardsDistributor.rewardTokenSupplySpeeds(vToken); - uint256 blockNumber = block.number; + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); - if (supplyState.lastRewardingBlock > 0 && blockNumber > supplyState.lastRewardingBlock) { - blockNumber = supplyState.lastRewardingBlock; + if ( + supplyState.lastRewardingBlockOrTimestamp > 0 && + blockNumberOrTimestamp > supplyState.lastRewardingBlockOrTimestamp + ) { + blockNumberOrTimestamp = supplyState.lastRewardingBlockOrTimestamp; } - uint256 deltaBlocks = sub_(blockNumber, uint256(supplyState.block)); - if (deltaBlocks > 0 && supplySpeed > 0) { + uint256 deltaBlocksOrTimestamp = sub_(blockNumberOrTimestamp, supplyState.blockOrTimestamp); + if (deltaBlocksOrTimestamp > 0 && supplySpeed > 0) { uint256 supplyTokens = VToken(vToken).totalSupply(); - uint256 tokensAccrued = mul_(deltaBlocks, supplySpeed); + uint256 tokensAccrued = mul_(deltaBlocksOrTimestamp, supplySpeed); Double memory ratio = supplyTokens > 0 ? fraction(tokensAccrued, supplyTokens) : Double({ mantissa: 0 }); Double memory index = add_(Double({ mantissa: supplyState.index }), ratio); supplyState.index = safe224(index.mantissa, "new index overflows"); - supplyState.block = safe32(blockNumber, "block number overflows"); - } else if (deltaBlocks > 0) { - supplyState.block = safe32(blockNumber, "block number overflows"); + supplyState.blockOrTimestamp = blockNumberOrTimestamp; + } else if (deltaBlocksOrTimestamp > 0) { + supplyState.blockOrTimestamp = blockNumberOrTimestamp; } } diff --git a/contracts/Rewards/RewardsDistributor.sol b/contracts/Rewards/RewardsDistributor.sol index 9fc2c9022..1eded6758 100644 --- a/contracts/Rewards/RewardsDistributor.sol +++ b/contracts/Rewards/RewardsDistributor.sol @@ -5,11 +5,13 @@ import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/acc import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; import { ExponentialNoError } from "../ExponentialNoError.sol"; import { VToken } from "../VToken.sol"; import { Comptroller } from "../Comptroller.sol"; import { MaxLoopsLimitHelper } from "../MaxLoopsLimitHelper.sol"; +import { RewardsDistributorStorage } from "./RewardsDistributorStorage.sol"; /** * @title `RewardsDistributor` @@ -18,60 +20,27 @@ import { MaxLoopsLimitHelper } from "../MaxLoopsLimitHelper.sol"; * Users can receive additional rewards through a `RewardsDistributor`. Each `RewardsDistributor` proxy is initialized with a specific reward * token and `Comptroller`, which can then distribute the reward token to users that supply or borrow in the associated pool. * Authorized users can set the reward token borrow and supply speeds for each market in the pool. This sets a fixed amount of reward - * token to be released each block for borrowers and suppliers, which is distributed based on a user’s percentage of the borrows or supplies + * token to be released each slot (block or second) for borrowers and suppliers, which is distributed based on a user’s percentage of the borrows or supplies * respectively. The owner can also set up reward distributions to contributor addresses (distinct from suppliers and borrowers) by setting - * their contributor reward token speed, which similarly allocates a fixed amount of reward token per block. + * their contributor reward token speed, which similarly allocates a fixed amount of reward token per slot (block or second). * * The owner has the ability to transfer any amount of reward tokens held by the contract to any other address. Rewards are not distributed * automatically and must be claimed by a user calling `claimRewardToken()`. Users should be aware that it is up to the owner and other centralized * entities to ensure that the `RewardsDistributor` holds enough tokens to distribute the accumulated rewards of users and contributors. */ -contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, AccessControlledV8, MaxLoopsLimitHelper { +contract RewardsDistributor is + ExponentialNoError, + Ownable2StepUpgradeable, + AccessControlledV8, + MaxLoopsLimitHelper, + RewardsDistributorStorage, + TimeManagerV8 +{ using SafeERC20Upgradeable for IERC20Upgradeable; - struct RewardToken { - // The market's last updated rewardTokenBorrowIndex or rewardTokenSupplyIndex - uint224 index; - // The block number the index was last updated at - uint32 block; - // The block number at which to stop rewards - uint32 lastRewardingBlock; - } - /// @notice The initial REWARD TOKEN index for a market uint224 public constant INITIAL_INDEX = 1e36; - /// @notice The REWARD TOKEN market supply state for each market - mapping(address => RewardToken) public rewardTokenSupplyState; - - /// @notice The REWARD TOKEN borrow index for each market for each supplier as of the last time they accrued REWARD TOKEN - mapping(address => mapping(address => uint256)) public rewardTokenSupplierIndex; - - /// @notice The REWARD TOKEN accrued but not yet transferred to each user - mapping(address => uint256) public rewardTokenAccrued; - - /// @notice The rate at which rewardToken is distributed to the corresponding borrow market (per block) - mapping(address => uint256) public rewardTokenBorrowSpeeds; - - /// @notice The rate at which rewardToken is distributed to the corresponding supply market (per block) - mapping(address => uint256) public rewardTokenSupplySpeeds; - - /// @notice The REWARD TOKEN market borrow state for each market - mapping(address => RewardToken) public rewardTokenBorrowState; - - /// @notice The portion of REWARD TOKEN that each contributor receives per block - mapping(address => uint256) public rewardTokenContributorSpeeds; - - /// @notice Last block at which a contributor's REWARD TOKEN rewards have been allocated - mapping(address => uint256) public lastContributorBlock; - - /// @notice The REWARD TOKEN borrow index for each market for each borrower as of the last time they accrued REWARD TOKEN - mapping(address => mapping(address => uint256)) public rewardTokenBorrowerIndex; - - Comptroller private comptroller; - - IERC20Upgradeable public rewardToken; - /// @notice Emitted when REWARD TOKEN is distributed to a supplier event DistributedSupplierRewardToken( VToken indexed vToken, @@ -120,13 +89,25 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce /// @notice Emitted when a reward token last rewarding block for borrow is updated event BorrowLastRewardingBlockUpdated(address indexed vToken, uint32 newBlock); + /// @notice Emitted when a reward token last rewarding timestamp for supply is updated + event SupplyLastRewardingBlockTimestampUpdated(address indexed vToken, uint256 newTimestamp); + + /// @notice Emitted when a reward token last rewarding timestamp for borrow is updated + event BorrowLastRewardingBlockTimestampUpdated(address indexed vToken, uint256 newTimestamp); + modifier onlyComptroller() { require(address(comptroller) == msg.sender, "Only comptroller can call this function"); _; } - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(bool timeBased_, uint256 blocksPerYear_) TimeManagerV8(timeBased_, blocksPerYear_) { + // Note that the contract is upgradeable. Use initialize() or reinitializers + // to set the state variables. _disableInitializers(); } @@ -152,29 +133,18 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce _setMaxLoopsLimit(loopsLimit_); } + /** + * @notice Initializes the market state for a specific vToken + * @param vToken The address of the vToken to be initialized + * @custom:event MarketInitialized emits on success + * @custom:access Only Comptroller + */ function initializeMarket(address vToken) external onlyComptroller { - uint32 blockNumber = safe32(getBlockNumber(), "block number exceeds 32 bits"); + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); - RewardToken storage supplyState = rewardTokenSupplyState[vToken]; - RewardToken storage borrowState = rewardTokenBorrowState[vToken]; - - /* - * Update market state indices - */ - if (supplyState.index == 0) { - // Initialize supply state index with default value - supplyState.index = INITIAL_INDEX; - } - - if (borrowState.index == 0) { - // Initialize borrow state index with default value - borrowState.index = INITIAL_INDEX; - } - - /* - * Update market state block numbers - */ - supplyState.block = borrowState.block = blockNumber; + isTimeBased + ? _initializeMarketTimestampBased(vToken, blockNumberOrTimestamp) + : _initializeMarketBlockBased(vToken, safe32(blockNumberOrTimestamp, "block number exceeds 32 bits")); emit MarketInitialized(vToken); } @@ -240,7 +210,7 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce } /** - * @notice Set REWARD TOKEN last rewarding block for the specified markets + * @notice Set REWARD TOKEN last rewarding block for the specified markets, used when contract is block based * @param vTokens The markets whose REWARD TOKEN last rewarding block to update * @param supplyLastRewardingBlocks New supply-side REWARD TOKEN last rewarding block for the corresponding market * @param borrowLastRewardingBlocks New borrow-side REWARD TOKEN last rewarding block for the corresponding market @@ -250,7 +220,9 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce uint32[] calldata supplyLastRewardingBlocks, uint32[] calldata borrowLastRewardingBlocks ) external { - _checkAccessAllowed("setLastRewardingBlock(address[],uint32[],uint32[])"); + _checkAccessAllowed("setLastRewardingBlocks(address[],uint32[],uint32[])"); + require(!isTimeBased, "Block-based operation only"); + uint256 numTokens = vTokens.length; require( numTokens == supplyLastRewardingBlocks.length && numTokens == borrowLastRewardingBlocks.length, @@ -265,6 +237,39 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce } } + /** + * @notice Set REWARD TOKEN last rewarding block timestamp for the specified markets, used when contract is time based + * @param vTokens The markets whose REWARD TOKEN last rewarding block to update + * @param supplyLastRewardingBlockTimestamps New supply-side REWARD TOKEN last rewarding block timestamp for the corresponding market + * @param borrowLastRewardingBlockTimestamps New borrow-side REWARD TOKEN last rewarding block timestamp for the corresponding market + */ + function setLastRewardingBlockTimestamps( + VToken[] calldata vTokens, + uint256[] calldata supplyLastRewardingBlockTimestamps, + uint256[] calldata borrowLastRewardingBlockTimestamps + ) external { + _checkAccessAllowed("setLastRewardingBlockTimestamps(address[],uint256[],uint256[])"); + require(isTimeBased, "Time-based operation only"); + + uint256 numTokens = vTokens.length; + require( + numTokens == supplyLastRewardingBlockTimestamps.length && + numTokens == borrowLastRewardingBlockTimestamps.length, + "RewardsDistributor::setLastRewardingBlockTimestamps invalid input" + ); + + for (uint256 i; i < numTokens; ) { + _setLastRewardingBlockTimestamp( + vTokens[i], + supplyLastRewardingBlockTimestamps[i], + borrowLastRewardingBlockTimestamps[i] + ); + unchecked { + ++i; + } + } + } + /** * @notice Set REWARD TOKEN speed for a single contributor * @param contributor The contributor whose REWARD TOKEN speed to update @@ -277,7 +282,7 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce // release storage delete lastContributorBlock[contributor]; } else { - lastContributorBlock[contributor] = getBlockNumber(); + lastContributorBlock[contributor] = getBlockNumberOrTimestamp(); } rewardTokenContributorSpeeds[contributor] = rewardTokenSpeed; @@ -310,14 +315,14 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce */ function updateContributorRewards(address contributor) public { uint256 rewardTokenSpeed = rewardTokenContributorSpeeds[contributor]; - uint256 blockNumber = getBlockNumber(); - uint256 deltaBlocks = sub_(blockNumber, lastContributorBlock[contributor]); - if (deltaBlocks > 0 && rewardTokenSpeed > 0) { - uint256 newAccrued = mul_(deltaBlocks, rewardTokenSpeed); + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); + uint256 deltaBlocksOrTimestamp = sub_(blockNumberOrTimestamp, lastContributorBlock[contributor]); + if (deltaBlocksOrTimestamp > 0 && rewardTokenSpeed > 0) { + uint256 newAccrued = mul_(deltaBlocksOrTimestamp, rewardTokenSpeed); uint256 contributorAccrued = add_(rewardTokenAccrued[contributor], newAccrued); rewardTokenAccrued[contributor] = contributorAccrued; - lastContributorBlock[contributor] = blockNumber; + lastContributorBlock[contributor] = blockNumberOrTimestamp; emit ContributorRewardsUpdated(contributor, rewardTokenAccrued[contributor]); } @@ -345,10 +350,6 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce rewardTokenAccrued[holder] = _grantRewardToken(holder, rewardTokenAccrued[holder]); } - function getBlockNumber() public view virtual returns (uint256) { - return block.number; - } - /** * @notice Set REWARD TOKEN last rewarding block for a single market. * @param vToken market's whose reward token last rewarding block to be updated @@ -362,7 +363,7 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce ) internal { require(comptroller.isMarketListed(vToken), "rewardToken market is not listed"); - uint256 blockNumber = getBlockNumber(); + uint256 blockNumber = getBlockNumberOrTimestamp(); require(supplyLastRewardingBlock > blockNumber, "setting last rewarding block in the past is not allowed"); require(borrowLastRewardingBlock > blockNumber, "setting last rewarding block in the past is not allowed"); @@ -390,6 +391,55 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce } } + /** + * @notice Set REWARD TOKEN last rewarding timestamp for a single market. + * @param vToken market's whose reward token last rewarding timestamp to be updated + * @param supplyLastRewardingBlockTimestamp New supply-side REWARD TOKEN last rewarding timestamp for market + * @param borrowLastRewardingBlockTimestamp New borrow-side REWARD TOKEN last rewarding timestamp for market + */ + function _setLastRewardingBlockTimestamp( + VToken vToken, + uint256 supplyLastRewardingBlockTimestamp, + uint256 borrowLastRewardingBlockTimestamp + ) internal { + require(comptroller.isMarketListed(vToken), "rewardToken market is not listed"); + + uint256 blockTimestamp = getBlockNumberOrTimestamp(); + + require( + supplyLastRewardingBlockTimestamp > blockTimestamp, + "setting last rewarding timestamp in the past is not allowed" + ); + require( + borrowLastRewardingBlockTimestamp > blockTimestamp, + "setting last rewarding timestamp in the past is not allowed" + ); + + uint256 currentSupplyLastRewardingBlockTimestamp = rewardTokenSupplyStateTimeBased[address(vToken)] + .lastRewardingTimestamp; + uint256 currentBorrowLastRewardingBlockTimestamp = rewardTokenBorrowStateTimeBased[address(vToken)] + .lastRewardingTimestamp; + + require( + currentSupplyLastRewardingBlockTimestamp == 0 || currentSupplyLastRewardingBlockTimestamp > blockTimestamp, + "this RewardsDistributor is already locked" + ); + require( + currentBorrowLastRewardingBlockTimestamp == 0 || currentBorrowLastRewardingBlockTimestamp > blockTimestamp, + "this RewardsDistributor is already locked" + ); + + if (currentSupplyLastRewardingBlockTimestamp != supplyLastRewardingBlockTimestamp) { + rewardTokenSupplyStateTimeBased[address(vToken)].lastRewardingTimestamp = supplyLastRewardingBlockTimestamp; + emit SupplyLastRewardingBlockTimestampUpdated(address(vToken), supplyLastRewardingBlockTimestamp); + } + + if (currentBorrowLastRewardingBlockTimestamp != borrowLastRewardingBlockTimestamp) { + rewardTokenBorrowStateTimeBased[address(vToken)].lastRewardingTimestamp = borrowLastRewardingBlockTimestamp; + emit BorrowLastRewardingBlockTimestampUpdated(address(vToken), borrowLastRewardingBlockTimestamp); + } + } + /** * @notice Set REWARD TOKEN speed for a single market. * @param vToken market's whose reward token rate to be updated @@ -430,7 +480,9 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce */ function _distributeSupplierRewardToken(address vToken, address supplier) internal { RewardToken storage supplyState = rewardTokenSupplyState[vToken]; - uint256 supplyIndex = supplyState.index; + TimeBasedRewardToken storage supplyStateTimeBased = rewardTokenSupplyStateTimeBased[vToken]; + + uint256 supplyIndex = isTimeBased ? supplyStateTimeBased.index : supplyState.index; uint256 supplierIndex = rewardTokenSupplierIndex[vToken][supplier]; // Update supplier's index to the current index since we are distributing accrued REWARD TOKEN @@ -465,7 +517,9 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce */ function _distributeBorrowerRewardToken(address vToken, address borrower, Exp memory marketBorrowIndex) internal { RewardToken storage borrowState = rewardTokenBorrowState[vToken]; - uint256 borrowIndex = borrowState.index; + TimeBasedRewardToken storage borrowStateTimeBased = rewardTokenBorrowStateTimeBased[vToken]; + + uint256 borrowIndex = isTimeBased ? borrowStateTimeBased.index : borrowState.index; uint256 borrowerIndex = rewardTokenBorrowerIndex[vToken][borrower]; // Update borrowers's index to the current index since we are distributing accrued REWARD TOKEN @@ -517,28 +571,50 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce */ function _updateRewardTokenSupplyIndex(address vToken) internal { RewardToken storage supplyState = rewardTokenSupplyState[vToken]; + TimeBasedRewardToken storage supplyStateTimeBased = rewardTokenSupplyStateTimeBased[vToken]; + uint256 supplySpeed = rewardTokenSupplySpeeds[vToken]; - uint32 blockNumber = safe32(getBlockNumber(), "block number exceeds 32 bits"); + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); - if (supplyState.lastRewardingBlock > 0 && blockNumber > supplyState.lastRewardingBlock) { - blockNumber = supplyState.lastRewardingBlock; + if (!isTimeBased) { + safe32(blockNumberOrTimestamp, "block number exceeds 32 bits"); } - uint256 deltaBlocks = sub_(uint256(blockNumber), uint256(supplyState.block)); + uint256 lastRewardingBlockOrTimestamp = isTimeBased + ? supplyStateTimeBased.lastRewardingTimestamp + : uint256(supplyState.lastRewardingBlock); - if (deltaBlocks > 0 && supplySpeed > 0) { + if (lastRewardingBlockOrTimestamp > 0 && blockNumberOrTimestamp > lastRewardingBlockOrTimestamp) { + blockNumberOrTimestamp = lastRewardingBlockOrTimestamp; + } + + uint256 deltaBlocksOrTimestamp = sub_( + blockNumberOrTimestamp, + (isTimeBased ? supplyStateTimeBased.timestamp : uint256(supplyState.block)) + ); + if (deltaBlocksOrTimestamp > 0 && supplySpeed > 0) { uint256 supplyTokens = VToken(vToken).totalSupply(); - uint256 accruedSinceUpdate = mul_(deltaBlocks, supplySpeed); + uint256 accruedSinceUpdate = mul_(deltaBlocksOrTimestamp, supplySpeed); Double memory ratio = supplyTokens > 0 ? fraction(accruedSinceUpdate, supplyTokens) : Double({ mantissa: 0 }); - supplyState.index = safe224( - add_(Double({ mantissa: supplyState.index }), ratio).mantissa, + uint224 supplyIndex = isTimeBased ? supplyStateTimeBased.index : supplyState.index; + uint224 index = safe224( + add_(Double({ mantissa: supplyIndex }), ratio).mantissa, "new index exceeds 224 bits" ); - supplyState.block = blockNumber; - } else if (deltaBlocks > 0) { - supplyState.block = blockNumber; + + if (isTimeBased) { + supplyStateTimeBased.index = index; + supplyStateTimeBased.timestamp = blockNumberOrTimestamp; + } else { + supplyState.index = index; + supplyState.block = uint32(blockNumberOrTimestamp); + } + } else if (deltaBlocksOrTimestamp > 0) { + isTimeBased ? supplyStateTimeBased.timestamp = blockNumberOrTimestamp : supplyState.block = uint32( + blockNumberOrTimestamp + ); } emit RewardTokenSupplyIndexUpdated(vToken); @@ -552,29 +628,110 @@ contract RewardsDistributor is ExponentialNoError, Ownable2StepUpgradeable, Acce */ function _updateRewardTokenBorrowIndex(address vToken, Exp memory marketBorrowIndex) internal { RewardToken storage borrowState = rewardTokenBorrowState[vToken]; + TimeBasedRewardToken storage borrowStateTimeBased = rewardTokenBorrowStateTimeBased[vToken]; + uint256 borrowSpeed = rewardTokenBorrowSpeeds[vToken]; - uint32 blockNumber = safe32(getBlockNumber(), "block number exceeds 32 bits"); + uint256 blockNumberOrTimestamp = getBlockNumberOrTimestamp(); - if (borrowState.lastRewardingBlock > 0 && blockNumber > borrowState.lastRewardingBlock) { - blockNumber = borrowState.lastRewardingBlock; + if (!isTimeBased) { + safe32(blockNumberOrTimestamp, "block number exceeds 32 bits"); } - uint256 deltaBlocks = sub_(uint256(blockNumber), uint256(borrowState.block)); - if (deltaBlocks > 0 && borrowSpeed > 0) { + uint256 lastRewardingBlockOrTimestamp = isTimeBased + ? borrowStateTimeBased.lastRewardingTimestamp + : uint256(borrowState.lastRewardingBlock); + + if (lastRewardingBlockOrTimestamp > 0 && blockNumberOrTimestamp > lastRewardingBlockOrTimestamp) { + blockNumberOrTimestamp = lastRewardingBlockOrTimestamp; + } + + uint256 deltaBlocksOrTimestamp = sub_( + blockNumberOrTimestamp, + (isTimeBased ? borrowStateTimeBased.timestamp : uint256(borrowState.block)) + ); + if (deltaBlocksOrTimestamp > 0 && borrowSpeed > 0) { uint256 borrowAmount = div_(VToken(vToken).totalBorrows(), marketBorrowIndex); - uint256 accruedSinceUpdate = mul_(deltaBlocks, borrowSpeed); + uint256 accruedSinceUpdate = mul_(deltaBlocksOrTimestamp, borrowSpeed); Double memory ratio = borrowAmount > 0 ? fraction(accruedSinceUpdate, borrowAmount) : Double({ mantissa: 0 }); - borrowState.index = safe224( - add_(Double({ mantissa: borrowState.index }), ratio).mantissa, + uint224 borrowIndex = isTimeBased ? borrowStateTimeBased.index : borrowState.index; + uint224 index = safe224( + add_(Double({ mantissa: borrowIndex }), ratio).mantissa, "new index exceeds 224 bits" ); - borrowState.block = blockNumber; - } else if (deltaBlocks > 0) { - borrowState.block = blockNumber; + + if (isTimeBased) { + borrowStateTimeBased.index = index; + borrowStateTimeBased.timestamp = blockNumberOrTimestamp; + } else { + borrowState.index = index; + borrowState.block = uint32(blockNumberOrTimestamp); + } + } else if (deltaBlocksOrTimestamp > 0) { + if (isTimeBased) { + borrowStateTimeBased.timestamp = blockNumberOrTimestamp; + } else { + borrowState.block = uint32(blockNumberOrTimestamp); + } } emit RewardTokenBorrowIndexUpdated(vToken, marketBorrowIndex); } + + /** + * @notice Initializes the market state for a specific vToken called when contract is block-based + * @param vToken The address of the vToken to be initialized + * @param blockNumber current block number + */ + function _initializeMarketBlockBased(address vToken, uint32 blockNumber) internal { + RewardToken storage supplyState = rewardTokenSupplyState[vToken]; + RewardToken storage borrowState = rewardTokenBorrowState[vToken]; + + /* + * Update market state indices + */ + if (supplyState.index == 0) { + // Initialize supply state index with default value + supplyState.index = INITIAL_INDEX; + } + + if (borrowState.index == 0) { + // Initialize borrow state index with default value + borrowState.index = INITIAL_INDEX; + } + + /* + * Update market state block numbers + */ + supplyState.block = borrowState.block = blockNumber; + } + + /** + * @notice Initializes the market state for a specific vToken called when contract is time-based + * @param vToken The address of the vToken to be initialized + * @param blockTimestamp current block timestamp + */ + function _initializeMarketTimestampBased(address vToken, uint256 blockTimestamp) internal { + TimeBasedRewardToken storage supplyState = rewardTokenSupplyStateTimeBased[vToken]; + TimeBasedRewardToken storage borrowState = rewardTokenBorrowStateTimeBased[vToken]; + + /* + * Update market state indices + */ + if (supplyState.index == 0) { + // Initialize supply state index with default value + supplyState.index = INITIAL_INDEX; + } + + if (borrowState.index == 0) { + // Initialize borrow state index with default value + borrowState.index = INITIAL_INDEX; + } + + /* + * Update market state block timestamp + */ + supplyState.timestamp = borrowState.timestamp = blockTimestamp; + } } diff --git a/contracts/Rewards/RewardsDistributorStorage.sol b/contracts/Rewards/RewardsDistributorStorage.sol new file mode 100644 index 000000000..90cd7904e --- /dev/null +++ b/contracts/Rewards/RewardsDistributorStorage.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +import { Comptroller } from "../Comptroller.sol"; + +/** + * @title RewardsDistributorStorage + * @author Venus + * @dev Storage for RewardsDistributor + */ +contract RewardsDistributorStorage { + struct RewardToken { + // The market's last updated rewardTokenBorrowIndex or rewardTokenSupplyIndex + uint224 index; + // The block number the index was last updated at + uint32 block; + // The block number at which to stop rewards + uint32 lastRewardingBlock; + } + + struct TimeBasedRewardToken { + // The market's last updated rewardTokenBorrowIndex or rewardTokenSupplyIndex + uint224 index; + // The block timestamp the index was last updated at + uint256 timestamp; + // The block timestamp at which to stop rewards + uint256 lastRewardingTimestamp; + } + + /// @notice The REWARD TOKEN market supply state for each market + mapping(address => RewardToken) public rewardTokenSupplyState; + + /// @notice The REWARD TOKEN borrow index for each market for each supplier as of the last time they accrued REWARD TOKEN + mapping(address => mapping(address => uint256)) public rewardTokenSupplierIndex; + + /// @notice The REWARD TOKEN accrued but not yet transferred to each user + mapping(address => uint256) public rewardTokenAccrued; + + /// @notice The rate at which rewardToken is distributed to the corresponding borrow market per slot (block or second) + mapping(address => uint256) public rewardTokenBorrowSpeeds; + + /// @notice The rate at which rewardToken is distributed to the corresponding supply market per slot (block or second) + mapping(address => uint256) public rewardTokenSupplySpeeds; + + /// @notice The REWARD TOKEN market borrow state for each market + mapping(address => RewardToken) public rewardTokenBorrowState; + + /// @notice The portion of REWARD TOKEN that each contributor receives per slot (block or second) + mapping(address => uint256) public rewardTokenContributorSpeeds; + + /// @notice Last slot (block or second) at which a contributor's REWARD TOKEN rewards have been allocated + mapping(address => uint256) public lastContributorBlock; + + /// @notice The REWARD TOKEN borrow index for each market for each borrower as of the last time they accrued REWARD TOKEN + mapping(address => mapping(address => uint256)) public rewardTokenBorrowerIndex; + + Comptroller internal comptroller; + + IERC20Upgradeable public rewardToken; + + /// @notice The REWARD TOKEN market supply state for each market + mapping(address => TimeBasedRewardToken) public rewardTokenSupplyStateTimeBased; + + /// @notice The REWARD TOKEN market borrow state for each market + mapping(address => TimeBasedRewardToken) public rewardTokenBorrowStateTimeBased; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[37] private __gap; +} diff --git a/contracts/Shortfall/Shortfall.sol b/contracts/Shortfall/Shortfall.sol index edfc7b462..31a3069a0 100644 --- a/contracts/Shortfall/Shortfall.sol +++ b/contracts/Shortfall/Shortfall.sol @@ -7,13 +7,16 @@ import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol"; import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; +import { ensureNonzeroAddress, ensureNonzeroValue } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; + import { VToken } from "../VToken.sol"; import { ComptrollerInterface, ComptrollerViewInterface } from "../ComptrollerInterface.sol"; import { IRiskFund } from "../RiskFund/IRiskFund.sol"; import { PoolRegistry } from "../Pool/PoolRegistry.sol"; import { PoolRegistryInterface } from "../Pool/PoolRegistryInterface.sol"; import { TokenDebtTracker } from "../lib/TokenDebtTracker.sol"; -import { ensureNonzeroAddress } from "../lib/validators.sol"; +import { ShortfallStorage } from "./ShortfallStorage.sol"; import { EXP_SCALE } from "../lib/constants.sol"; /** @@ -26,74 +29,34 @@ import { EXP_SCALE } from "../lib/constants.sol"; * if the risk fund covers the pool's bad debt plus the 10% incentive, then the auction winner is determined by who will take the smallest percentage of the * risk fund in exchange for paying off all the pool's bad debt. */ -contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGuardUpgradeable, TokenDebtTracker { +contract Shortfall is + Ownable2StepUpgradeable, + AccessControlledV8, + ReentrancyGuardUpgradeable, + TokenDebtTracker, + ShortfallStorage, + TimeManagerV8 +{ using SafeERC20Upgradeable for IERC20Upgradeable; - /// @notice Type of auction - enum AuctionType { - LARGE_POOL_DEBT, - LARGE_RISK_FUND - } - - /// @notice Status of auction - enum AuctionStatus { - NOT_STARTED, - STARTED, - ENDED - } - - /// @notice Auction metadata - struct Auction { - uint256 startBlock; - AuctionType auctionType; - AuctionStatus status; - VToken[] markets; - uint256 seizedRiskFund; - address highestBidder; - uint256 highestBidBps; - uint256 highestBidBlock; - uint256 startBidBps; - mapping(VToken => uint256) marketDebt; - mapping(VToken => uint256) bidAmount; - } - /// @dev Max basis points i.e., 100% uint256 private constant MAX_BPS = 10000; - uint256 private constant DEFAULT_NEXT_BIDDER_BLOCK_LIMIT = 100; - - uint256 private constant DEFAULT_WAIT_FOR_FIRST_BIDDER = 100; - - uint256 private constant DEFAULT_INCENTIVE_BPS = 1000; // 10% - - /// @notice Pool registry address - address public poolRegistry; - - /// @notice Risk fund address - IRiskFund public riskFund; - - /// @notice Minimum USD debt in pool for shortfall to trigger - uint256 public minimumPoolBadDebt; - - /// @notice Incentive to auction participants, initial value set to 1000 or 10% - uint256 public incentiveBps; + // @notice Default incentive basis points (BPS) for the auction participants, set to 10% + uint256 private constant DEFAULT_INCENTIVE_BPS = 1000; - /// @notice Time to wait for next bidder. Initially waits for 100 blocks - uint256 public nextBidderBlockLimit; + // @notice Default block or timestamp limit for the next bidder to place a bid + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint256 private immutable DEFAULT_NEXT_BIDDER_BLOCK_OR_TIMESTAMP_LIMIT; - /// @notice Boolean of if auctions are paused - bool public auctionsPaused; - - /// @notice Time to wait for first bidder. Initially waits for 100 blocks - uint256 public waitForFirstBidder; - - /// @notice Auctions for each pool - mapping(address => Auction) public auctions; + // @notice Default number of blocks or seconds to wait for the first bidder before starting the auction + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint256 private immutable DEFAULT_WAIT_FOR_FIRST_BIDDER; /// @notice Emitted when a auction starts event AuctionStarted( address indexed comptroller, - uint256 auctionStartBlock, + uint256 auctionStartBlockOrTimestamp, AuctionType auctionType, VToken[] markets, uint256[] marketsDebt, @@ -102,12 +65,17 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua ); /// @notice Emitted when a bid is placed - event BidPlaced(address indexed comptroller, uint256 auctionStartBlock, uint256 bidBps, address indexed bidder); + event BidPlaced( + address indexed comptroller, + uint256 auctionStartBlockOrTimestamp, + uint256 bidBps, + address indexed bidder + ); /// @notice Emitted when a auction is completed event AuctionClosed( address indexed comptroller, - uint256 auctionStartBlock, + uint256 auctionStartBlockOrTimestamp, address indexed highestBidder, uint256 highestBidBps, uint256 seizedRiskFind, @@ -116,7 +84,7 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua ); /// @notice Emitted when a auction is restarted - event AuctionRestarted(address indexed comptroller, uint256 auctionStartBlock); + event AuctionRestarted(address indexed comptroller, uint256 auctionStartBlockOrTimestamp); /// @notice Emitted when pool registry address is updated event PoolRegistryUpdated(address indexed oldPoolRegistry, address indexed newPoolRegistry); @@ -124,11 +92,14 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua /// @notice Emitted when minimum pool bad debt is updated event MinimumPoolBadDebtUpdated(uint256 oldMinimumPoolBadDebt, uint256 newMinimumPoolBadDebt); - /// @notice Emitted when wait for first bidder block count is updated + /// @notice Emitted when wait for first bidder block or timestamp count is updated event WaitForFirstBidderUpdated(uint256 oldWaitForFirstBidder, uint256 newWaitForFirstBidder); - /// @notice Emitted when next bidder block limit is updated - event NextBidderBlockLimitUpdated(uint256 oldNextBidderBlockLimit, uint256 newNextBidderBlockLimit); + /// @notice Emitted when next bidder block or timestamp limit is updated + event NextBidderBlockLimitUpdated( + uint256 oldNextBidderBlockOrTimestampLimit, + uint256 newNextBidderBlockOrTimestampLimit + ); /// @notice Emitted when incentiveBps is updated event IncentiveBpsUpdated(uint256 oldIncentiveBps, uint256 newIncentiveBps); @@ -139,8 +110,25 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua /// @notice Emitted when auctions are unpaused event AuctionsResumed(address sender); - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @param nextBidderBlockOrTimestampLimit_ Default block or timestamp limit for the next bidder to place a bid + * @param waitForFirstBidder_ Default number of blocks or seconds to wait for the first bidder before starting the auction + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bool timeBased_, + uint256 blocksPerYear_, + uint256 nextBidderBlockOrTimestampLimit_, + uint256 waitForFirstBidder_ + ) TimeManagerV8(timeBased_, blocksPerYear_) { + ensureNonzeroValue(nextBidderBlockOrTimestampLimit_); + ensureNonzeroValue(waitForFirstBidder_); + + DEFAULT_NEXT_BIDDER_BLOCK_OR_TIMESTAMP_LIMIT = nextBidderBlockOrTimestampLimit_; + DEFAULT_WAIT_FOR_FIRST_BIDDER = waitForFirstBidder_; + // Note that the contract is upgradeable. Use initialize() or reinitializers // to set the state variables. _disableInitializers(); @@ -168,23 +156,24 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua __TokenDebtTracker_init(); minimumPoolBadDebt = minimumPoolBadDebt_; riskFund = riskFund_; - waitForFirstBidder = DEFAULT_WAIT_FOR_FIRST_BIDDER; - nextBidderBlockLimit = DEFAULT_NEXT_BIDDER_BLOCK_LIMIT; incentiveBps = DEFAULT_INCENTIVE_BPS; auctionsPaused = false; + + waitForFirstBidder = DEFAULT_WAIT_FOR_FIRST_BIDDER; + nextBidderBlockLimit = DEFAULT_NEXT_BIDDER_BLOCK_OR_TIMESTAMP_LIMIT; } /** * @notice Place a bid greater than the previous in an ongoing auction * @param comptroller Comptroller address of the pool * @param bidBps The bid percent of the risk fund or bad debt depending on auction type - * @param auctionStartBlock The block number when auction started + * @param auctionStartBlockOrTimestamp The block number or timestamp when auction started * @custom:event Emits BidPlaced event on success */ - function placeBid(address comptroller, uint256 bidBps, uint256 auctionStartBlock) external nonReentrant { + function placeBid(address comptroller, uint256 bidBps, uint256 auctionStartBlockOrTimestamp) external nonReentrant { Auction storage auction = auctions[comptroller]; - require(auction.startBlock == auctionStartBlock, "auction has been restarted"); + require(auction.startBlockOrTimestamp == auctionStartBlockOrTimestamp, "auction has been restarted"); require(_isStarted(auction), "no on-going auction"); require(!_isStale(auction), "auction is stale, restart it"); require(bidBps > 0, "basis points cannot be zero"); @@ -222,9 +211,9 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua auction.highestBidder = msg.sender; auction.highestBidBps = bidBps; - auction.highestBidBlock = block.number; + auction.highestBidBlockOrTimestamp = getBlockNumberOrTimestamp(); - emit BidPlaced(comptroller, auction.startBlock, bidBps, msg.sender); + emit BidPlaced(comptroller, auction.startBlockOrTimestamp, bidBps, msg.sender); } /** @@ -237,7 +226,8 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua require(_isStarted(auction), "no on-going auction"); require( - block.number > auction.highestBidBlock + nextBidderBlockLimit && auction.highestBidder != address(0), + getBlockNumberOrTimestamp() > auction.highestBidBlockOrTimestamp + nextBidderBlockLimit && + auction.highestBidder != address(0), "waiting for next bidder. cannot close auction" ); @@ -273,7 +263,7 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua emit AuctionClosed( comptroller, - auction.startBlock, + auction.startBlockOrTimestamp, auction.highestBidder, auction.highestBidBps, transferredAmount, @@ -307,62 +297,62 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua auction.status = AuctionStatus.ENDED; - emit AuctionRestarted(comptroller, auction.startBlock); + emit AuctionRestarted(comptroller, auction.startBlockOrTimestamp); _startAuction(comptroller); } /** - * @notice Update next bidder block limit which is used determine when an auction can be closed - * @param _nextBidderBlockLimit New next bidder block limit + * @notice Update next bidder block or timestamp limit which is used determine when an auction can be closed + * @param nextBidderBlockOrTimestampLimit_ New next bidder slot (block or second) limit * @custom:event Emits NextBidderBlockLimitUpdated on success * @custom:access Restricted by ACM */ - function updateNextBidderBlockLimit(uint256 _nextBidderBlockLimit) external { + function updateNextBidderBlockLimit(uint256 nextBidderBlockOrTimestampLimit_) external { _checkAccessAllowed("updateNextBidderBlockLimit(uint256)"); - require(_nextBidderBlockLimit != 0, "_nextBidderBlockLimit must not be 0"); - uint256 oldNextBidderBlockLimit = nextBidderBlockLimit; - nextBidderBlockLimit = _nextBidderBlockLimit; - emit NextBidderBlockLimitUpdated(oldNextBidderBlockLimit, _nextBidderBlockLimit); + require(nextBidderBlockOrTimestampLimit_ != 0, "nextBidderBlockOrTimestampLimit_ must not be 0"); + + emit NextBidderBlockLimitUpdated(nextBidderBlockLimit, nextBidderBlockOrTimestampLimit_); + nextBidderBlockLimit = nextBidderBlockOrTimestampLimit_; } /** * @notice Updates the incentive BPS - * @param _incentiveBps New incentive BPS + * @param incentiveBps_ New incentive BPS * @custom:event Emits IncentiveBpsUpdated on success * @custom:access Restricted by ACM */ - function updateIncentiveBps(uint256 _incentiveBps) external { + function updateIncentiveBps(uint256 incentiveBps_) external { _checkAccessAllowed("updateIncentiveBps(uint256)"); - require(_incentiveBps != 0, "incentiveBps must not be 0"); + require(incentiveBps_ != 0, "incentiveBps must not be 0"); uint256 oldIncentiveBps = incentiveBps; - incentiveBps = _incentiveBps; - emit IncentiveBpsUpdated(oldIncentiveBps, _incentiveBps); + incentiveBps = incentiveBps_; + emit IncentiveBpsUpdated(oldIncentiveBps, incentiveBps_); } /** * @notice Update minimum pool bad debt to start auction - * @param _minimumPoolBadDebt Minimum bad debt in the base asset for a pool to start auction + * @param minimumPoolBadDebt_ Minimum bad debt in the base asset for a pool to start auction * @custom:event Emits MinimumPoolBadDebtUpdated on success * @custom:access Restricted by ACM */ - function updateMinimumPoolBadDebt(uint256 _minimumPoolBadDebt) external { + function updateMinimumPoolBadDebt(uint256 minimumPoolBadDebt_) external { _checkAccessAllowed("updateMinimumPoolBadDebt(uint256)"); uint256 oldMinimumPoolBadDebt = minimumPoolBadDebt; - minimumPoolBadDebt = _minimumPoolBadDebt; - emit MinimumPoolBadDebtUpdated(oldMinimumPoolBadDebt, _minimumPoolBadDebt); + minimumPoolBadDebt = minimumPoolBadDebt_; + emit MinimumPoolBadDebtUpdated(oldMinimumPoolBadDebt, minimumPoolBadDebt_); } /** - * @notice Update wait for first bidder block count. If the first bid is not made within this limit, the auction is closed and needs to be restarted - * @param _waitForFirstBidder New wait for first bidder block count + * @notice Update wait for first bidder block or timestamp count. If the first bid is not made within this limit, the auction is closed and needs to be restarted + * @param waitForFirstBidder_ New wait for first bidder block or timestamp count * @custom:event Emits WaitForFirstBidderUpdated on success * @custom:access Restricted by ACM */ - function updateWaitForFirstBidder(uint256 _waitForFirstBidder) external { + function updateWaitForFirstBidder(uint256 waitForFirstBidder_) external { _checkAccessAllowed("updateWaitForFirstBidder(uint256)"); uint256 oldWaitForFirstBidder = waitForFirstBidder; - waitForFirstBidder = _waitForFirstBidder; - emit WaitForFirstBidderUpdated(oldWaitForFirstBidder, _waitForFirstBidder); + waitForFirstBidder = waitForFirstBidder_; + emit WaitForFirstBidderUpdated(oldWaitForFirstBidder, waitForFirstBidder_); } /** @@ -421,7 +411,7 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua ); auction.highestBidBps = 0; - auction.highestBidBlock = 0; + auction.highestBidBlockOrTimestamp = 0; uint256 marketsCount = auction.markets.length; for (uint256 i; i < marketsCount; ++i) { @@ -473,13 +463,13 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua } auction.seizedRiskFund = riskFundBalance - remainingRiskFundBalance; - auction.startBlock = block.number; + auction.startBlockOrTimestamp = getBlockNumberOrTimestamp(); auction.status = AuctionStatus.STARTED; auction.highestBidder = address(0); emit AuctionStarted( comptroller, - auction.startBlock, + auction.startBlockOrTimestamp, auction.auctionType, auction.markets, marketsDebt, @@ -517,12 +507,12 @@ contract Shortfall is Ownable2StepUpgradeable, AccessControlledV8, ReentrancyGua /** * @dev Checks if the auction is stale, i.e. there's no bidder and the auction - * was started more than waitForFirstBidder blocks ago. + * was started more than waitForFirstBidder blocks or seconds ago. * @param auction The auction to query the status for * @return True if the auction is stale */ function _isStale(Auction storage auction) internal view returns (bool) { bool noBidder = auction.highestBidder == address(0); - return noBidder && (block.number > auction.startBlock + waitForFirstBidder); + return noBidder && (getBlockNumberOrTimestamp() > auction.startBlockOrTimestamp + waitForFirstBidder); } } diff --git a/contracts/Shortfall/ShortfallStorage.sol b/contracts/Shortfall/ShortfallStorage.sol new file mode 100644 index 000000000..15e045ec7 --- /dev/null +++ b/contracts/Shortfall/ShortfallStorage.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VToken } from "../VToken.sol"; +import { IRiskFund } from "../RiskFund/IRiskFund.sol"; + +/** + * @title ShortfallStorage + * @author Venus + * @dev Storage for Shortfall + */ +contract ShortfallStorage { + /// @notice Type of auction + enum AuctionType { + LARGE_POOL_DEBT, + LARGE_RISK_FUND + } + + /// @notice Status of auction + enum AuctionStatus { + NOT_STARTED, + STARTED, + ENDED + } + + /// @notice Auction metadata + struct Auction { + /// @notice It holds either the starting block number or timestamp + uint256 startBlockOrTimestamp; + AuctionType auctionType; + AuctionStatus status; + VToken[] markets; + uint256 seizedRiskFund; + address highestBidder; + uint256 highestBidBps; + /// @notice It holds either the highestBid block or timestamp + uint256 highestBidBlockOrTimestamp; + uint256 startBidBps; + mapping(VToken => uint256) marketDebt; + mapping(VToken => uint256) bidAmount; + } + + /// @notice Pool registry address + address public poolRegistry; + + /// @notice Risk fund address + IRiskFund public riskFund; + + /// @notice Minimum USD debt in pool for shortfall to trigger + uint256 public minimumPoolBadDebt; + + /// @notice Incentive to auction participants, initial value set to 1000 or 10% + uint256 public incentiveBps; + + /// @notice Time to wait for next bidder. Initially waits for DEFAULT_NEXT_BIDDER_BLOCK_OR_TIMESTAMP_LIMIT + uint256 public nextBidderBlockLimit; + + /// @notice Boolean of if auctions are paused + bool public auctionsPaused; + + /// @notice Time to wait for first bidder. Initially waits for DEFAULT_WAIT_FOR_FIRST_BIDDER + uint256 public waitForFirstBidder; + + /// @notice Auctions for each pool + mapping(address => Auction) public auctions; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[42] private __gap; +} diff --git a/contracts/VToken.sol b/contracts/VToken.sol index b46b92e43..0e12b02f7 100644 --- a/contracts/VToken.sol +++ b/contracts/VToken.sol @@ -12,6 +12,7 @@ import { ComptrollerInterface, ComptrollerViewInterface } from "./ComptrollerInt import { TokenErrorReporter } from "./ErrorReporter.sol"; import { InterestRateModel } from "./InterestRateModel.sol"; import { ExponentialNoError } from "./ExponentialNoError.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; /** @@ -45,12 +46,20 @@ contract VToken is AccessControlledV8, VTokenInterface, ExponentialNoError, - TokenErrorReporter + TokenErrorReporter, + TimeManagerV8 { using SafeERC20Upgradeable for IERC20Upgradeable; uint256 internal constant DEFAULT_PROTOCOL_SEIZE_SHARE_MANTISSA = 5e16; // 5% + // Maximum fraction of interest that can be set aside for reserves + uint256 internal constant MAX_RESERVE_FACTOR_MANTISSA = 1e18; + + // Maximum borrow rate that can ever be applied per slot(block or second) + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint256 internal immutable MAX_BORROW_RATE_MANTISSA; + /** * Reentrancy Guard ** */ @@ -65,10 +74,22 @@ contract VToken is _notEntered = true; // get a gas-refund post-Istanbul } - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @param maxBorrowRateMantissa_ The maximum value of borrowing rate mantissa + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bool timeBased_, + uint256 blocksPerYear_, + uint256 maxBorrowRateMantissa_ + ) TimeManagerV8(timeBased_, blocksPerYear_) { // Note that the contract is upgradeable. Use initialize() or reinitializers // to set the state variables. + require(maxBorrowRateMantissa_ <= 1e18, "Max borrow rate must be <= 1e18"); + + MAX_BORROW_RATE_MANTISSA = maxBorrowRateMantissa_; _disableInitializers(); } @@ -487,7 +508,7 @@ contract VToken is */ function reduceReserves(uint256 reduceAmount) external override nonReentrant { accrueInterest(); - if (reduceReservesBlockNumber == _getBlockNumber()) return; + if (reduceReservesBlockNumber == getBlockNumberOrTimestamp()) return; _reduceReservesFresh(reduceAmount); } @@ -677,15 +698,15 @@ contract VToken is } /** - * @notice A public function to set new threshold of block difference after which funds will be sent to the protocol share reserve - * @param _newReduceReservesBlockDelta block difference value + * @notice A public function to set new threshold of slot(block or second) difference after which funds will be sent to the protocol share reserve + * @param _newReduceReservesBlockOrTimestampDelta slot(block or second) difference value * @custom:access Only Governance */ - function setReduceReservesBlockDelta(uint256 _newReduceReservesBlockDelta) external { + function setReduceReservesBlockDelta(uint256 _newReduceReservesBlockOrTimestampDelta) external { _checkAccessAllowed("setReduceReservesBlockDelta(uint256)"); - require(_newReduceReservesBlockDelta > 0, "Invalid Input"); - emit NewReduceReservesBlockDelta(reduceReservesBlockDelta, _newReduceReservesBlockDelta); - reduceReservesBlockDelta = _newReduceReservesBlockDelta; + require(_newReduceReservesBlockOrTimestampDelta > 0, "Invalid Input"); + emit NewReduceReservesBlockDelta(reduceReservesBlockDelta, _newReduceReservesBlockOrTimestampDelta); + reduceReservesBlockDelta = _newReduceReservesBlockOrTimestampDelta; } /** @@ -736,16 +757,16 @@ contract VToken is } /** - * @notice Returns the current per-block borrow interest rate for this vToken - * @return rate The borrow interest rate per block, scaled by 1e18 + * @notice Returns the current per slot(block or second) borrow interest rate for this vToken + * @return rate The borrow interest rate per slot(block or second), scaled by 1e18 */ function borrowRatePerBlock() external view override returns (uint256) { return interestRateModel.getBorrowRate(_getCashPrior(), totalBorrows, totalReserves, badDebt); } /** - * @notice Returns the current per-block supply interest rate for this v - * @return rate The supply interest rate per block, scaled by 1e18 + * @notice Returns the current per-slot(block or second) supply interest rate for this v + * @return rate The supply interest rate per slot(block or second), scaled by 1e18 */ function supplyRatePerBlock() external view override returns (uint256) { return @@ -787,21 +808,21 @@ contract VToken is /** * @notice Applies accrued interest to total borrows and reserves - * @dev This calculates interest accrued from the last checkpointed block - * up to the current block and writes new checkpoint to storage and + * @dev This calculates interest accrued from the last checkpointed slot(block or second) + * up to the current slot(block or second) and writes new checkpoint to storage and * reduce spread reserves to protocol share reserve - * if currentBlock - reduceReservesBlockNumber >= blockDelta + * if currentSlot - reduceReservesBlockNumber >= slotDelta * @return Always NO_ERROR * @custom:event Emits AccrueInterest event on success * @custom:access Not restricted */ function accrueInterest() public virtual override returns (uint256) { - /* Remember the initial block number */ - uint256 currentBlockNumber = _getBlockNumber(); - uint256 accrualBlockNumberPrior = accrualBlockNumber; + /* Remember the initial block number or timestamp */ + uint256 currentSlotNumber = getBlockNumberOrTimestamp(); + uint256 accrualSlotNumberPrior = accrualBlockNumber; /* Short-circuit accumulating 0 interest */ - if (accrualBlockNumberPrior == currentBlockNumber) { + if (accrualSlotNumberPrior == currentSlotNumber) { return NO_ERROR; } @@ -815,19 +836,19 @@ contract VToken is uint256 borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior, badDebt); require(borrowRateMantissa <= MAX_BORROW_RATE_MANTISSA, "borrow rate is absurdly high"); - /* Calculate the number of blocks elapsed since the last accrual */ - uint256 blockDelta = currentBlockNumber - accrualBlockNumberPrior; + /* Calculate the number of slots elapsed since the last accrual */ + uint256 slotDelta = currentSlotNumber - accrualSlotNumberPrior; /* * Calculate the interest accumulated into borrows and reserves and the new index: - * simpleInterestFactor = borrowRate * blockDelta + * simpleInterestFactor = borrowRate * slotDelta * interestAccumulated = simpleInterestFactor * totalBorrows * totalBorrowsNew = interestAccumulated + totalBorrows * totalReservesNew = interestAccumulated * reserveFactor + totalReserves * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex */ - Exp memory simpleInterestFactor = mul_(Exp({ mantissa: borrowRateMantissa }), blockDelta); + Exp memory simpleInterestFactor = mul_(Exp({ mantissa: borrowRateMantissa }), slotDelta); uint256 interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior); uint256 totalBorrowsNew = interestAccumulated + borrowsPrior; uint256 totalReservesNew = mul_ScalarTruncateAddUInt( @@ -842,13 +863,13 @@ contract VToken is // (No safe failures beyond this point) /* We write the previously calculated values into storage */ - accrualBlockNumber = currentBlockNumber; + accrualBlockNumber = currentSlotNumber; borrowIndex = borrowIndexNew; totalBorrows = totalBorrowsNew; totalReserves = totalReservesNew; - if (currentBlockNumber - reduceReservesBlockNumber >= reduceReservesBlockDelta) { - reduceReservesBlockNumber = currentBlockNumber; + if (currentSlotNumber - reduceReservesBlockNumber >= reduceReservesBlockDelta) { + reduceReservesBlockNumber = currentSlotNumber; if (cashPrior < totalReservesNew) { _reduceReservesFresh(cashPrior); } else { @@ -864,7 +885,7 @@ contract VToken is /** * @notice User supplies assets into the market and receives vTokens in exchange - * @dev Assumes interest has already been accrued up to the current block + * @dev Assumes interest has already been accrued up to the current block or timestamp * @param payer The address of the account which is sending the assets for supply * @param minter The address of the account which is supplying the assets * @param mintAmount The amount of the underlying asset to supply @@ -873,8 +894,8 @@ contract VToken is /* Fail if mint not allowed */ comptroller.preMintHook(address(this), minter, mintAmount); - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != _getBlockNumber()) { + /* Verify market's slot(block or second) number equals current slot(block or second) number */ + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert MintFreshnessCheck(); } @@ -921,7 +942,7 @@ contract VToken is /** * @notice Redeemer redeems vTokens in exchange for the underlying assets, transferred to the receiver. Redeemer and receiver can be the same * address, or different addresses if the receiver was previously approved by the redeemer as a valid delegate (see Comptroller.updateDelegate) - * @dev Assumes interest has already been accrued up to the current block + * @dev Assumes interest has already been accrued up to the current slot(block or second) * @param redeemer The address of the account which is redeeming the tokens * @param receiver The receiver of the underlying tokens * @param redeemTokensIn The number of vTokens to redeem into underlying (only one of redeemTokensIn or redeemAmountIn may be non-zero) @@ -930,8 +951,8 @@ contract VToken is function _redeemFresh(address redeemer, address receiver, uint256 redeemTokensIn, uint256 redeemAmountIn) internal { require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero"); - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != _getBlockNumber()) { + /* Verify market's slot(block or second) number equals current slot(block or second) number */ + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert RedeemFreshnessCheck(); } @@ -1012,8 +1033,8 @@ contract VToken is /* Fail if borrow not allowed */ comptroller.preBorrowHook(address(this), borrower, borrowAmount); - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != _getBlockNumber()) { + /* Verify market's slot(block or second) number equals current slot(block or second) number */ + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert BorrowFreshnessCheck(); } @@ -1068,8 +1089,8 @@ contract VToken is /* Fail if repayBorrow not allowed */ comptroller.preRepayHook(address(this), borrower); - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != _getBlockNumber()) { + /* Verify market's slot(block or second) number equals current slot(block or second) number */ + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert RepayBorrowFreshnessCheck(); } @@ -1166,13 +1187,13 @@ contract VToken is skipLiquidityCheck ); - /* Verify market's block number equals current block number */ - if (accrualBlockNumber != _getBlockNumber()) { + /* Verify market's slot(block or second) number equals current slot(block or second) number */ + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert LiquidateFreshnessCheck(); } - /* Verify vTokenCollateral market's block number equals current block number */ - if (vTokenCollateral.accrualBlockNumber() != _getBlockNumber()) { + /* Verify vTokenCollateral market's slot(block or second) number equals current slot(block or second) number */ + if (vTokenCollateral.accrualBlockNumber() != getBlockNumberOrTimestamp()) { revert LiquidateCollateralFreshnessCheck(); } @@ -1307,8 +1328,8 @@ contract VToken is * @param newReserveFactorMantissa New reserve factor (from 0 to 1e18) */ function _setReserveFactorFresh(uint256 newReserveFactorMantissa) internal { - // Verify market's block number equals current block number - if (accrualBlockNumber != _getBlockNumber()) { + // Verify market's slot(block or second) number equals current slot(block or second) number + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert SetReserveFactorFreshCheck(); } @@ -1334,8 +1355,8 @@ contract VToken is uint256 totalReservesNew; uint256 actualAddAmount; - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != _getBlockNumber()) { + // We fail gracefully unless market's slot(block or second) number equals current slot(block or second) number + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert AddReservesFactorFreshCheck(actualAddAmount); } @@ -1359,8 +1380,8 @@ contract VToken is // totalReserves - reduceAmount uint256 totalReservesNew; - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != _getBlockNumber()) { + // We fail gracefully unless market's slot(block or second) number equals current slot(block or second) number + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert ReduceReservesFreshCheck(); } @@ -1406,8 +1427,8 @@ contract VToken is // Used to store old model for use in the event that is emitted on success InterestRateModel oldInterestRateModel; - // We fail gracefully unless market's block number equals current block number - if (accrualBlockNumber != _getBlockNumber()) { + // We fail gracefully unless market's slot(block or second) number equals current slot(block or second) number + if (accrualBlockNumber != getBlockNumberOrTimestamp()) { revert SetInterestRateModelFreshCheck(); } @@ -1539,11 +1560,11 @@ contract VToken is _setComptroller(comptroller_); - // Initialize block number and borrow index (block number mocks depend on comptroller being set) - accrualBlockNumber = _getBlockNumber(); + // Initialize slot(block or second) number and borrow index (slot(block or second) number mocks depend on comptroller being set) + accrualBlockNumber = getBlockNumberOrTimestamp(); borrowIndex = MANTISSA_ONE; - // Set the interest rate model (depends on block number / borrow index) + // Set the interest rate model (depends on slot(block or second) number / borrow index) _setInterestRateModelFresh(interestRateModel_); _setReserveFactorFresh(reserveFactorMantissa_); @@ -1593,15 +1614,6 @@ contract VToken is return IERC20Upgradeable(underlying).balanceOf(address(this)); } - /** - * @dev Function to simply retrieve block number - * This exists mainly for inheriting test contracts to stub this result. - * @return Current block number - */ - function _getBlockNumber() internal view virtual returns (uint256) { - return block.number; - } - /** * @notice Return the borrow balance of account based on stored data * @param account The address whose balance should be calculated diff --git a/contracts/VTokenInterfaces.sol b/contracts/VTokenInterfaces.sol index e97e1eeb5..0f01e1f17 100644 --- a/contracts/VTokenInterfaces.sol +++ b/contracts/VTokenInterfaces.sol @@ -54,12 +54,6 @@ contract VTokenStorage { */ address payable public protocolShareReserve; - // Maximum borrow rate that can ever be applied (.0005% / block) - uint256 internal constant MAX_BORROW_RATE_MANTISSA = 0.0005e16; - - // Maximum fraction of interest that can be set aside for reserves - uint256 internal constant MAX_RESERVE_FACTOR_MANTISSA = 1e18; - /** * @notice Contract which oversees inter-vToken operations */ @@ -79,7 +73,7 @@ contract VTokenStorage { uint256 public reserveFactorMantissa; /** - * @notice Block number that interest was last accrued at + * @notice Slot(block or second) number that interest was last accrued at */ uint256 public accrualBlockNumber; @@ -128,12 +122,12 @@ contract VTokenStorage { address public shortfall; /** - * @notice delta block after which reserves will be reduced + * @notice delta slot (block or second) after which reserves will be reduced */ uint256 public reduceReservesBlockDelta; /** - * @notice last block number at which reserves were reduced + * @notice last slot (block or second) number at which reserves were reduced */ uint256 public reduceReservesBlockNumber; @@ -282,9 +276,12 @@ abstract contract VTokenInterface is VTokenStorage { event SweepToken(address indexed token); /** - * @notice Event emitted when reduce reserves block delta is changed + * @notice Event emitted when reduce reserves slot (block or second) delta is changed */ - event NewReduceReservesBlockDelta(uint256 oldReduceReservesBlockDelta, uint256 newReduceReservesBlockDelta); + event NewReduceReservesBlockDelta( + uint256 oldReduceReservesBlockOrTimestampDelta, + uint256 newReduceReservesBlockOrTimestampDelta + ); /** * @notice Event emitted when liquidation reserves are reduced diff --git a/contracts/WhitePaperInterestRateModel.sol b/contracts/WhitePaperInterestRateModel.sol index 574adfa74..2b0858020 100644 --- a/contracts/WhitePaperInterestRateModel.sol +++ b/contracts/WhitePaperInterestRateModel.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.25; import { InterestRateModel } from "./InterestRateModel.sol"; +import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; import { EXP_SCALE, MANTISSA_ONE } from "./lib/constants.sol"; /** @@ -9,44 +10,45 @@ import { EXP_SCALE, MANTISSA_ONE } from "./lib/constants.sol"; * @author Compound * @notice The parameterized model described in section 2.4 of the original Compound Protocol whitepaper */ -contract WhitePaperInterestRateModel is InterestRateModel { +contract WhitePaperInterestRateModel is InterestRateModel, TimeManagerV8 { /** - * @notice The approximate number of blocks per year that is assumed by the interest rate model - */ - uint256 public immutable blocksPerYear; - /** - * @notice The multiplier of utilization rate that gives the slope of the interest rate + * @notice The multiplier of utilization rate per block or second that gives the slope of the interest rate */ uint256 public immutable multiplierPerBlock; /** - * @notice The base interest rate which is the y-intercept when utilization rate is 0 + * @notice The base interest rate per block or second which is the y-intercept when utilization rate is 0 */ uint256 public immutable baseRatePerBlock; - event NewInterestParams(uint256 baseRatePerBlock, uint256 multiplierPerBlock); + event NewInterestParams(uint256 baseRatePerBlockOrTimestamp, uint256 multiplierPerBlockOrTimestamp); /** * @notice Construct an interest rate model - * @param baseRatePerYear The approximate target base APR, as a mantissa (scaled by EXP_SCALE) - * @param multiplierPerYear The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) + * @param baseRatePerYear_ The approximate target base APR, as a mantissa (scaled by EXP_SCALE) + * @param multiplierPerYear_ The rate of increase in interest rate wrt utilization (scaled by EXP_SCALE) + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year */ - constructor(uint256 blocksPerYear_, uint256 baseRatePerYear, uint256 multiplierPerYear) { - require(blocksPerYear_ != 0, "Invalid blocks per year"); - baseRatePerBlock = baseRatePerYear / blocksPerYear_; - multiplierPerBlock = multiplierPerYear / blocksPerYear_; - blocksPerYear = blocksPerYear_; + constructor( + uint256 baseRatePerYear_, + uint256 multiplierPerYear_, + bool timeBased_, + uint256 blocksPerYear_ + ) TimeManagerV8(timeBased_, blocksPerYear_) { + baseRatePerBlock = baseRatePerYear_ / blocksOrSecondsPerYear; + multiplierPerBlock = multiplierPerYear_ / blocksOrSecondsPerYear; emit NewInterestParams(baseRatePerBlock, multiplierPerBlock); } /** - * @notice Calculates the current borrow rate per block, with the error code expected by the market + * @notice Calculates the current borrow rate per slot(block/second), with the error code expected by the market * @param cash The amount of cash in the market * @param borrows The amount of borrows in the market * @param reserves The amount of reserves in the market * @param badDebt The amount of badDebt in the market - * @return The borrow rate percentage per block as a mantissa (scaled by EXP_SCALE) + * @return The borrow rate percentage per slot(block/second) as a mantissa (scaled by EXP_SCALE) */ function getBorrowRate( uint256 cash, @@ -59,13 +61,13 @@ contract WhitePaperInterestRateModel is InterestRateModel { } /** - * @notice Calculates the current supply rate per block + * @notice Calculates the current supply rate per slot(block/second) * @param cash The amount of cash in the market * @param borrows The amount of borrows in the market * @param reserves The amount of reserves in the market * @param reserveFactorMantissa The current reserve factor for the market * @param badDebt The amount of badDebt in the market - * @return The supply rate percentage per block as a mantissa (scaled by EXP_SCALE) + * @return The supply rate percentage per slot(block/second) as a mantissa (scaled by EXP_SCALE) */ function getSupplyRate( uint256 cash, diff --git a/contracts/lib/constants.sol b/contracts/lib/constants.sol index 9022efac1..b95b34329 100644 --- a/contracts/lib/constants.sol +++ b/contracts/lib/constants.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: BSD-3-Clause pragma solidity ^0.8.25; +/// @dev The approximate number of seconds per year +uint256 constant SECONDS_PER_YEAR = 31_536_000; + /// @dev Base unit for computations, usually used in scaling (multiplications, divisions) uint256 constant EXP_SCALE = 1e18; diff --git a/contracts/test/UpgradedVToken.sol b/contracts/test/UpgradedVToken.sol index de86227ca..efae5ac4f 100644 --- a/contracts/test/UpgradedVToken.sol +++ b/contracts/test/UpgradedVToken.sol @@ -13,6 +13,21 @@ import { InterestRateModel } from "../InterestRateModel.sol"; * @author Venus */ contract UpgradedVToken is VToken { + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bool timeBased_, + uint256 blocksPerYear_, + uint256 maxBorrowRateMantissa_ + ) VToken(timeBased_, blocksPerYear_, maxBorrowRateMantissa_) { + // Note that the contract is upgradeable. Use initialize() or reinitializers + // to set the state variables. + _disableInitializers(); + } + /** * @notice Construct a new money market * @param underlying_ The address of the underlying asset diff --git a/contracts/test/VTokenHarness.sol b/contracts/test/VTokenHarness.sol index a9a878067..5797527ff 100644 --- a/contracts/test/VTokenHarness.sol +++ b/contracts/test/VTokenHarness.sol @@ -13,6 +13,21 @@ contract VTokenHarness is VToken { mapping(address => bool) public failTransferToAddresses; + /** + * @param timeBased_ A boolean indicating whether the contract is based on time or block. + * @param blocksPerYear_ The number of blocks per year + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + bool timeBased_, + uint256 blocksPerYear_, + uint256 maxBorrowRateMantissa_ + ) VToken(timeBased_, blocksPerYear_, maxBorrowRateMantissa_) { + // Note that the contract is upgradeable. Use initialize() or reinitializers + // to set the state variables. + _disableInitializers(); + } + function harnessSetAccrualBlockNumber(uint256 accrualBlockNumber_) external { accrualBlockNumber = accrualBlockNumber_; } @@ -107,7 +122,7 @@ contract VTokenHarness is VToken { return (snapshot.principal, snapshot.interestIndex); } - function getBorrowRateMaxMantissa() external pure returns (uint256) { + function getBorrowRateMaxMantissa() external view returns (uint256) { return MAX_BORROW_RATE_MANTISSA; } @@ -119,6 +134,10 @@ contract VTokenHarness is VToken { comptroller.preBorrowHook(address(this), msg.sender, amount); } + function getBlockNumberOrTimestamp() public view override returns (uint256) { + return blockNumber; + } + function _doTransferOut(address to, uint256 amount) internal override { require(failTransferToAddresses[to] == false, "HARNESS_TOKEN_TRANSFER_OUT_FAILED"); return super._doTransferOut(to, amount); @@ -130,8 +149,4 @@ contract VTokenHarness is VToken { } return super._exchangeRateStored(); } - - function _getBlockNumber() internal view override returns (uint256) { - return blockNumber; - } } diff --git a/deploy/007-deploy-pool-lens.ts b/deploy/007-deploy-pool-lens.ts index 1222f368a..2ccd22246 100644 --- a/deploy/007-deploy-pool-lens.ts +++ b/deploy/007-deploy-pool-lens.ts @@ -1,14 +1,18 @@ import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { getBlockOrTimestampBasedDeploymentInfo } from "../helpers/deploymentUtils"; + const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deploy } = deployments; const { deployer } = await getNamedAccounts(); + const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(hre.network.name); + await deploy("PoolLens", { from: deployer, - args: [], + args: [isTimeBased, blocksPerYear], log: true, autoMine: true, }); diff --git a/deploy/009-deploy-vtokens.ts b/deploy/009-deploy-vtokens.ts index 62f51e341..d7c6b9fce 100644 --- a/deploy/009-deploy-vtokens.ts +++ b/deploy/009-deploy-vtokens.ts @@ -6,9 +6,9 @@ import { DeployResult } from "hardhat-deploy/dist/types"; import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { blocksPerYear, getConfig, getTokenConfig } from "../helpers/deploymentConfig"; +import { getConfig, getMaxBorrowRateMantissa, getTokenConfig } from "../helpers/deploymentConfig"; import { InterestRateModels } from "../helpers/deploymentConfig"; -import { getUnregisteredVTokens, toAddress } from "../helpers/deploymentUtils"; +import { getBlockOrTimestampBasedDeploymentInfo, getUnregisteredVTokens, toAddress } from "../helpers/deploymentUtils"; import { AddressOne } from "../helpers/utils"; const mantissaToBps = (num: BigNumberish) => { @@ -21,6 +21,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployer } = await getNamedAccounts(); const { tokensConfig, poolConfig, preconfiguredAddresses } = await getConfig(hre.network.name); + const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(hre.network.name); + const maxBorrowRateMantissa = getMaxBorrowRateMantissa(hre.network.name); + const accessControlManagerAddress = await toAddress( preconfiguredAddresses.AccessControlManager || "AccessControlManager", hre, @@ -30,7 +33,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const vTokenImpl: DeployResult = await deploy("VTokenImpl", { contract: "VToken", from: deployer, - args: [], + args: [isTimeBased, blocksPerYear, maxBorrowRateMantissa], log: true, autoMine: true, skipIfAlreadyDeployed: true, @@ -75,7 +78,6 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { } let rateModelAddress: string; - const BLOCKS_PER_YEAR: number = blocksPerYear[hre.network.name]; if (rateModel === InterestRateModels.JumpRate.toString()) { const [b, m, j, k] = [baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_].map(mantissaToBps); const rateModelName = `JumpRateModelV2_base${b}bps_slope${m}bps_jump${j}bps_kink${k}bps`; @@ -84,12 +86,13 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { from: deployer, contract: "JumpRateModelV2", args: [ - BLOCKS_PER_YEAR, baseRatePerYear, multiplierPerYear, jumpMultiplierPerYear, kink_, accessControlManagerAddress, + isTimeBased, + blocksPerYear, ], log: true, autoMine: true, @@ -103,7 +106,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const result: DeployResult = await deploy(rateModelName, { from: deployer, contract: "WhitePaperInterestRateModel", - args: [BLOCKS_PER_YEAR, baseRatePerYear, multiplierPerYear], + args: [baseRatePerYear, multiplierPerYear, isTimeBased, blocksPerYear], log: true, autoMine: true, skipIfAlreadyDeployed: true, diff --git a/deploy/010-deploy-reward-distributors.ts b/deploy/010-deploy-reward-distributors.ts index 34001a258..c05e1e820 100644 --- a/deploy/010-deploy-reward-distributors.ts +++ b/deploy/010-deploy-reward-distributors.ts @@ -3,7 +3,11 @@ import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { getConfig, getTokenAddress, getTokenConfig } from "../helpers/deploymentConfig"; -import { getUnregisteredRewardsDistributors, toAddress } from "../helpers/deploymentUtils"; +import { + getBlockOrTimestampBasedDeploymentInfo, + getUnregisteredRewardsDistributors, + toAddress, +} from "../helpers/deploymentUtils"; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; @@ -12,6 +16,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const maxLoopsLimit = 100; const { tokensConfig, poolConfig, preconfiguredAddresses } = await getConfig(hre.network.name); + const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(hre.network.name); const accessControlAddress = await toAddress( preconfiguredAddresses.AccessControlManager || "AccessControlManager", @@ -25,6 +30,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { contract: "RewardsDistributor", from: deployer, autoMine: true, + args: [isTimeBased, blocksPerYear], log: true, skipIfAlreadyDeployed: true, }); @@ -52,6 +58,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }, upgradeIndex: 0, }, + args: [isTimeBased, blocksPerYear], autoMine: true, log: true, skipIfAlreadyDeployed: true, diff --git a/deploy/014-riskfund-protocolshare.ts b/deploy/014-riskfund-protocolshare.ts index 86f413d8d..860adf347 100644 --- a/deploy/014-riskfund-protocolshare.ts +++ b/deploy/014-riskfund-protocolshare.ts @@ -2,8 +2,8 @@ import { ethers } from "hardhat"; import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { getConfig } from "../helpers/deploymentConfig"; -import { getUnderlyingToken, toAddress } from "../helpers/deploymentUtils"; +import { getBidderDeploymentValues, getConfig } from "../helpers/deploymentConfig"; +import { getBlockOrTimestampBasedDeploymentInfo, getUnderlyingToken, toAddress } from "../helpers/deploymentUtils"; import { convertToUnit } from "../helpers/utils"; const MIN_AMOUNT_TO_CONVERT = convertToUnit(10, 18); @@ -17,6 +17,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { tokensConfig } = await getConfig(hre.network.name); const usdt = await getUnderlyingToken("USDT", tokensConfig); + const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(hre.network.name); + + const { waitForFirstBidder, nextBidderBlockOrTimestampLimit } = getBidderDeploymentValues(hre.network.name); + const poolRegistry = await ethers.getContract("PoolRegistry"); const deployerSigner = ethers.provider.getSigner(deployer); @@ -58,6 +62,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { }, upgradeIndex: 0, }, + args: [isTimeBased, blocksPerYear, nextBidderBlockOrTimestampLimit, waitForFirstBidder], autoMine: true, log: true, }); diff --git a/deploy/016-deploy-beacon-implementations.ts b/deploy/016-deploy-beacon-implementations.ts index e720e1bce..36ca1451a 100644 --- a/deploy/016-deploy-beacon-implementations.ts +++ b/deploy/016-deploy-beacon-implementations.ts @@ -2,12 +2,19 @@ import { ethers } from "hardhat"; import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { getMaxBorrowRateMantissa } from "../helpers/deploymentConfig"; +import { getBlockOrTimestampBasedDeploymentInfo } from "../helpers/deploymentUtils"; + // This deploy script deploys implementations for Comptroller and/or VToken that should be updated through a VIP afterwards const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployments, getNamedAccounts } = hre; const { deploy } = deployments; const { deployer } = await getNamedAccounts(); + + const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(hre.network.name); + const poolRegistry = await ethers.getContract("PoolRegistry"); + const maxBorrowRateMantissa = getMaxBorrowRateMantissa(hre.network.name); // Comptroller Implementation await deploy("ComptrollerImpl", { @@ -22,7 +29,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await deploy("VTokenImpl", { contract: "VToken", from: deployer, - args: [], + args: [isTimeBased, blocksPerYear, maxBorrowRateMantissa], log: true, autoMine: true, }); diff --git a/hardhat.config.ts b/hardhat.config.ts index a85e2d076..d3b7945aa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -221,9 +221,10 @@ const config: HardhatUserConfig = { bscmainnet: { url: process.env.ARCHIVE_NODE_bscmainnet || "https://bsc-dataseed.binance.org/", chainId: 56, - live: true, - timeout: 1200000, // 20 minutes - accounts: process.env.DEPLOYER_PRIVATE_KEY ? [`0x${process.env.DEPLOYER_PRIVATE_KEY}`] : [], + timeout: 1200000, + accounts: { + mnemonic: process.env.MNEMONIC || "", + }, }, ethereum: { url: process.env.ARCHIVE_NODE_ethereum || "https://ethereum.blockpi.network/v1/rpc/public", @@ -273,6 +274,22 @@ const config: HardhatUserConfig = { browserURL: "https://bscscan.com", }, }, + { + network: "sepolia", + chainId: 11155111, + urls: { + apiURL: "https://api-sepolia.etherscan.io/api", + browserURL: "https://sepolia.etherscan.io", + }, + }, + { + network: "ethereum", + chainId: 1, + urls: { + apiURL: "https://api.etherscan.io/api", + browserURL: "https://etherscan.io", + }, + }, { network: "opbnbtestnet", chainId: 5611, @@ -301,10 +318,10 @@ const config: HardhatUserConfig = { apiKey: { bscmainnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", bsctestnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", - opbnbtestnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", - opbnbmainnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", - sepolia: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", ethereum: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", + sepolia: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", + opbnbmainnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", + opbnbtestnet: process.env.ETHERSCAN_API_KEY || "ETHERSCAN_API_KEY", }, }, paths: { diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index dc3f8c4a6..ab38015dd 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -10,6 +10,7 @@ import { contracts as venusProtocolEthereum } from "@venusprotocol/venus-protoco import { contracts as venusProtocolOpbnbMainnet } from "@venusprotocol/venus-protocol/deployments/opbnbmainnet.json"; import { contracts as venusProtocolOpbnbTestnet } from "@venusprotocol/venus-protocol/deployments/opbnbtestnet.json"; import { contracts as venusProtocolSepolia } from "@venusprotocol/venus-protocol/deployments/sepolia.json"; +import { BigNumber } from "ethers"; import { ethers } from "hardhat"; import { DeploymentsExtension } from "hardhat-deploy/types"; @@ -34,6 +35,16 @@ export type DeploymentConfig = { preconfiguredAddresses: PreconfiguredAddresses; }; +export type DeploymentInfo = { + isTimeBased: boolean; + blocksPerYear: number; +}; + +type BidderDeploymentValues = { + waitForFirstBidder: number; + nextBidderBlockOrTimestampLimit: number; +}; + export type TokenConfig = { isMock: boolean; name?: string; @@ -98,9 +109,10 @@ export enum InterestRateModels { const ANY_CONTRACT = ethers.constants.AddressZero; -const BSC_BLOCKS_PER_YEAR = 10_512_000; // assuming a block is mined every 3 seconds -const ETH_BLOCKS_PER_YEAR = 2_628_000; // assuming a block is mined every 12 seconds -const OPBNB_BLOCKS_PER_YEAR = 31_536_000; // assuming a block is mined every 1 seconds +export const BSC_BLOCKS_PER_YEAR = 10_512_000; // assuming a block is mined every 3 seconds +export const ETH_BLOCKS_PER_YEAR = 2_628_000; // assuming a block is mined every 12 seconds +export const OPBNB_BLOCKS_PER_YEAR = 31_536_000; // assuming a block is mined every 1 seconds +export const SECONDS_PER_YEAR = 31_536_000; // seconds per year export type BlocksPerYear = { [key: string]: number; @@ -114,6 +126,7 @@ export const blocksPerYear: BlocksPerYear = { ethereum: ETH_BLOCKS_PER_YEAR, opbnbtestnet: OPBNB_BLOCKS_PER_YEAR, opbnbmainnet: OPBNB_BLOCKS_PER_YEAR, + isTimeBased: 0, }; export const SEPOLIA_MULTISIG = "0x94fa6078b6b8a26f0b6edffbe6501b22a10470fb"; @@ -3457,3 +3470,88 @@ export async function getTokenAddress(tokenConfig: TokenConfig, deployments: Dep return tokenConfig.tokenAddress; } } + +export function getBidderDeploymentValues(networkName: string): BidderDeploymentValues { + const isTimeBased = process.env.IS_TIME_BASED_DEPLOYMENT === "true"; + + if (isTimeBased) { + return { + waitForFirstBidder: 300, + nextBidderBlockOrTimestampLimit: 300, + }; + } + + switch (networkName) { + case "hardhat": + return { + waitForFirstBidder: 100, + nextBidderBlockOrTimestampLimit: 100, + }; + case "bsctestnet": + return { + waitForFirstBidder: 100, + nextBidderBlockOrTimestampLimit: 100, + }; + case "bscmainnet": + return { + waitForFirstBidder: 100, + nextBidderBlockOrTimestampLimit: 100, + }; + case "sepolia": + return { + waitForFirstBidder: 25, + nextBidderBlockOrTimestampLimit: 25, + }; + case "ethereum": + return { + waitForFirstBidder: 25, + nextBidderBlockOrTimestampLimit: 25, + }; + case "opbnbtestnet": + return { + waitForFirstBidder: 300, + nextBidderBlockOrTimestampLimit: 300, + }; + case "opbnbmainnet": + return { + waitForFirstBidder: 300, + nextBidderBlockOrTimestampLimit: 300, + }; + case "development": + return { + waitForFirstBidder: 100, + nextBidderBlockOrTimestampLimit: 100, + }; + default: + throw new Error(`bidder limits for network ${networkName} is not available.`); + } +} + +export function getMaxBorrowRateMantissa(networkName: string): BigNumber { + const isTimeBased = process.env.IS_TIME_BASED_DEPLOYMENT === "true"; + + if (isTimeBased) { + return BigNumber.from(0.00016667e16); // (0.0005e16 / 3) for per second + } + + switch (networkName) { + case "hardhat": + return BigNumber.from(0.0005e16); + case "bsctestnet": + return BigNumber.from(0.0005e16); + case "bscmainnet": + return BigNumber.from(0.0005e16); + case "sepolia": + return BigNumber.from(0.0005e16); + case "ethereum": + return BigNumber.from(0.0005e16); + case "opbnbtestnet": + return BigNumber.from(0.0005e16); + case "opbnbmainnet": + return BigNumber.from(0.0005e16); + case "development": + return BigNumber.from(0.0005e16); + default: + throw new Error(`max borrow rate for network ${networkName} is not available.`); + } +} diff --git a/helpers/deploymentUtils.ts b/helpers/deploymentUtils.ts index 3686e01ee..38f017cfc 100644 --- a/helpers/deploymentUtils.ts +++ b/helpers/deploymentUtils.ts @@ -2,7 +2,15 @@ import { ethers } from "hardhat"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { Comptroller, ERC20, MockToken } from "../typechain"; -import { PoolConfig, RewardConfig, TokenConfig, VTokenConfig, getTokenConfig } from "./deploymentConfig"; +import { + DeploymentInfo, + PoolConfig, + RewardConfig, + TokenConfig, + VTokenConfig, + blocksPerYear, + getTokenConfig, +} from "./deploymentConfig"; export const toAddress = async (addressOrAlias: string, hre: HardhatRuntimeEnvironment): Promise => { const { getNamedAccounts } = hre; @@ -126,3 +134,10 @@ export const getUnregisteredRewardsDistributors = async ( }), ); }; + +export const getBlockOrTimestampBasedDeploymentInfo = (network: string): DeploymentInfo => { + const isTimeBased = process.env.IS_TIME_BASED_DEPLOYMENT === "true"; + const blocksPerYearKey = isTimeBased ? "isTimeBased" : network; + + return { isTimeBased: isTimeBased, blocksPerYear: blocksPerYear[blocksPerYearKey] }; +}; diff --git a/package.json b/package.json index 63c473dff..de9217301 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@openzeppelin/contracts-upgradeable": "^4.8.3", "@openzeppelin/hardhat-upgrades": "^1.21.0", "@solidity-parser/parser": "^0.13.2", + "@venusprotocol/solidity-utilities": "^2.0.0", "ethers": "^5.7.0", "hardhat-deploy": "^0.11.14", "module-alias": "^2.2.2" diff --git a/tests/hardhat/Fork/RiskFundSwap.ts b/tests/hardhat/Fork/RiskFundSwap.ts index b6f019472..4d64af916 100644 --- a/tests/hardhat/Fork/RiskFundSwap.ts +++ b/tests/hardhat/Fork/RiskFundSwap.ts @@ -105,11 +105,11 @@ const riskFundFixture = async (): Promise => { fakeAccessControlManager.isAllowedToCall.returns(true); const Shortfall = await ethers.getContractFactory("Shortfall"); - const shortfall = await upgrades.deployProxy(Shortfall, [ - AddressOne, - parseUnits("10000", 18), - fakeAccessControlManager.address, - ]); + const shortfall = await upgrades.deployProxy( + Shortfall, + [AddressOne, parseUnits("10000", 18), fakeAccessControlManager.address], + { constructorArgs: [false, 10512000, 100, 100] }, + ); const fakeCorePoolComptroller = await smock.fake("Comptroller"); diff --git a/tests/hardhat/Fork/Shortfall.ts b/tests/hardhat/Fork/Shortfall.ts index cc9ae4cfb..35f743931 100644 --- a/tests/hardhat/Fork/Shortfall.ts +++ b/tests/hardhat/Fork/Shortfall.ts @@ -30,6 +30,7 @@ const { expect } = chai; chai.use(smock.matchers); const FORK_MAINNET = process.env.FORK === "true" && process.env.FORKED_NETWORK === "bscmainnet"; + const ADMIN = "0x939bD8d64c0A9583A7Dcea9933f7b21697ab6396"; const TREASURY = "0xF322942f644A996A617BD29c16bd7d231d9F35E9"; const ACM = "0x4788629ABc6cFCA10F9f969efdEAa1cF70c23555"; diff --git a/tests/hardhat/Fork/reduceReservesTest.ts b/tests/hardhat/Fork/reduceReservesTest.ts index 7fff6767f..e5adc8c6f 100644 --- a/tests/hardhat/Fork/reduceReservesTest.ts +++ b/tests/hardhat/Fork/reduceReservesTest.ts @@ -23,6 +23,7 @@ chai.use(smock.matchers); const FORK_TESTNET = process.env.FORK === "true" && process.env.FORKED_NETWORK === "bsctestnet"; const FORK_MAINNET = process.env.FORK === "true" && process.env.FORKED_NETWORK === "bscmainnet"; + let ADMIN: string; let ACM: string; let acc1: string; diff --git a/tests/hardhat/JumpRateModelV2.ts b/tests/hardhat/JumpRateModelV2.ts index d379fb8bb..ba9b69865 100644 --- a/tests/hardhat/JumpRateModelV2.ts +++ b/tests/hardhat/JumpRateModelV2.ts @@ -4,14 +4,15 @@ import BigNumber from "bignumber.js"; import chai from "chai"; import { ethers } from "hardhat"; -import { blocksPerYear as blocksPerYearConfig } from "../../helpers/deploymentConfig"; +import { BSC_BLOCKS_PER_YEAR } from "../../helpers/deploymentConfig"; import { convertToUnit } from "../../helpers/utils"; import { AccessControlManager, JumpRateModelV2 } from "../../typechain"; +import { getDescription } from "./util/descriptionHelpers"; const { expect } = chai; chai.use(smock.matchers); -describe("Jump rate model tests", () => { +for (const isTimeBased of [false, true]) { let jumpRateModel: JumpRateModelV2; let accessControlManager: FakeContract; @@ -21,109 +22,128 @@ describe("Jump rate model tests", () => { const reserves = convertToUnit(2, 19); const badDebt = convertToUnit(1, 19); const expScale = convertToUnit(1, 18); - const blocksPerYear = blocksPerYearConfig.hardhat; const baseRatePerYear = convertToUnit(2, 12); const multiplierPerYear = convertToUnit(4, 14); const jumpMultiplierPerYear = convertToUnit(2, 18); - const fixture = async () => { - accessControlManager = await smock.fake("AccessControlManager"); - accessControlManager.isAllowedToCall.returns(true); - - const JumpRateModelFactory = await ethers.getContractFactory("JumpRateModelV2"); - jumpRateModel = await JumpRateModelFactory.deploy( - blocksPerYear, - baseRatePerYear, - multiplierPerYear, - jumpMultiplierPerYear, - kink, - accessControlManager.address, - ); - await jumpRateModel.deployed(); - }; - - before(async () => { - await loadFixture(fixture); + const description = getDescription(isTimeBased); + let slotsPerYear = isTimeBased ? 0 : BSC_BLOCKS_PER_YEAR; + + describe(`${description}Jump rate model tests`, async () => { + const fixture = async () => { + accessControlManager = await smock.fake("AccessControlManager"); + accessControlManager.isAllowedToCall.returns(true); + const JumpRateModelFactory = await ethers.getContractFactory("JumpRateModelV2"); + + jumpRateModel = await JumpRateModelFactory.deploy( + baseRatePerYear, + multiplierPerYear, + jumpMultiplierPerYear, + kink, + accessControlManager.address, + isTimeBased, + slotsPerYear, + ); + await jumpRateModel.deployed(); + }; + + before(async () => { + await loadFixture(fixture); + slotsPerYear = (await jumpRateModel.blocksOrSecondsPerYear()).toNumber(); + }); + + it("Update jump rate model", async () => { + let baseRatePerBlockOrTimestamp = new BigNumber(baseRatePerYear).dividedBy(slotsPerYear).toFixed(0); + let multiplierPerBlockOrTimestamp = new BigNumber(multiplierPerYear) + .dividedBy(new BigNumber(slotsPerYear)) + .toFixed(0); + let jumpMultiplierPerBlockOrTimestamp = new BigNumber(jumpMultiplierPerYear).dividedBy(slotsPerYear).toFixed(0); + + expect(await jumpRateModel.baseRatePerBlock()).equal(baseRatePerBlockOrTimestamp); + expect(await jumpRateModel.multiplierPerBlock()).equal(multiplierPerBlockOrTimestamp); + expect(await jumpRateModel.jumpMultiplierPerBlock()).equal(jumpMultiplierPerBlockOrTimestamp); + expect(await jumpRateModel.kink()).equal(kink); + + await jumpRateModel.updateJumpRateModel(convertToUnit(3, 12), convertToUnit(5, 14), convertToUnit(2.2, 18), kink); + + baseRatePerBlockOrTimestamp = new BigNumber(convertToUnit(3, 12)).dividedBy(slotsPerYear).toFixed(0); + multiplierPerBlockOrTimestamp = new BigNumber(convertToUnit(5, 14)) + .dividedBy(new BigNumber(slotsPerYear)) + .toFixed(0); + jumpMultiplierPerBlockOrTimestamp = new BigNumber(convertToUnit(2.2, 18)).dividedBy(slotsPerYear).toFixed(0); + + expect(await jumpRateModel.baseRatePerBlock()).equal(baseRatePerBlockOrTimestamp); + expect(await jumpRateModel.multiplierPerBlock()).equal(multiplierPerBlockOrTimestamp); + expect(await jumpRateModel.jumpMultiplierPerBlock()).equal(jumpMultiplierPerBlockOrTimestamp); + expect(await jumpRateModel.kink()).equal(kink); + }); + + it("Utilization rate: borrows and badDebt is zero", async () => { + expect(await jumpRateModel.utilizationRate(cash, 0, reserves, 0)).equal(0); + }); + + it("Should return correct number of blocks", async () => { + expect(await jumpRateModel.blocksOrSecondsPerYear()).to.equal(slotsPerYear); + }); + + it("Utilization rate", async () => { + const utilizationRate = new BigNumber(Number(borrows) + Number(badDebt)) + .multipliedBy(expScale) + .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)) + .toFixed(0); + + expect(await jumpRateModel.utilizationRate(cash, borrows, reserves, badDebt)).equal(utilizationRate); + }); + + it("Borrow Rate: below kink utilization", async () => { + const multiplierPerBlockOrTimestamp = (await jumpRateModel.multiplierPerBlock()).toString(); + const baseRatePerBlockOrTimestamp = (await jumpRateModel.baseRatePerBlock()).toString(); + const utilizationRate = (await jumpRateModel.utilizationRate(cash, borrows, reserves, badDebt)).toString(); + + const value = new BigNumber(utilizationRate) + .multipliedBy(multiplierPerBlockOrTimestamp) + .dividedBy(expScale) + .toFixed(0); + + expect(await jumpRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).equal( + Number(value) + Number(baseRatePerBlockOrTimestamp), + ); + }); + + it("Borrow Rate: above kink utilization", async () => { + const multiplierPerBlockOrTimestamp = (await jumpRateModel.multiplierPerBlock()).toString(); + const jumpMultiplierPerBlockOrTimestamp = (await jumpRateModel.jumpMultiplierPerBlock()).toString(); + const baseRatePerBlockOrTimestamp = (await jumpRateModel.baseRatePerBlock()).toString(); + const utilizationRate = ( + await jumpRateModel.utilizationRate(convertToUnit(6, 19), convertToUnit(16, 19), reserves, badDebt) + ).toString(); + + const value = new BigNumber(kink).multipliedBy(multiplierPerBlockOrTimestamp).dividedBy(expScale).toFixed(0); + + const normalRate = Number(value) + Number(baseRatePerBlockOrTimestamp); + const excessUtil = Number(utilizationRate) - Number(kink); + + const jumpValue = new BigNumber(excessUtil) + .multipliedBy(jumpMultiplierPerBlockOrTimestamp) + .dividedBy(expScale) + .toFixed(0); + + expect(await jumpRateModel.getBorrowRate(convertToUnit(6, 19), convertToUnit(16, 19), reserves, badDebt)).equal( + Number(jumpValue) + Number(normalRate), + ); + }); + + it("Supply Rate", async () => { + const reserveMantissa = convertToUnit(1, 17); + const oneMinusReserveFactor = Number(expScale) - Number(reserveMantissa); + const borrowRate = (await jumpRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).toString(); + const rateToPool = new BigNumber(borrowRate).multipliedBy(oneMinusReserveFactor).dividedBy(expScale).toFixed(0); + const rate = new BigNumber(borrows) + .multipliedBy(expScale) + .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)); + const supplyRate = new BigNumber(rateToPool).multipliedBy(rate).dividedBy(expScale).toFixed(0); + + expect(await jumpRateModel.getSupplyRate(cash, borrows, reserves, reserveMantissa, badDebt)).equal(supplyRate); + }); }); - - it("Update jump rate model", async () => { - let baseRatePerBlock = new BigNumber(baseRatePerYear).dividedBy(blocksPerYear).toFixed(0); - let multiplierPerBlock = new BigNumber(multiplierPerYear).dividedBy(new BigNumber(blocksPerYear)).toFixed(0); - let jumpMultiplierPerBlock = new BigNumber(jumpMultiplierPerYear).dividedBy(blocksPerYear).toFixed(0); - - expect(await jumpRateModel.blocksPerYear()).equal(blocksPerYear); - expect(await jumpRateModel.baseRatePerBlock()).equal(baseRatePerBlock); - expect(await jumpRateModel.multiplierPerBlock()).equal(multiplierPerBlock); - expect(await jumpRateModel.jumpMultiplierPerBlock()).equal(jumpMultiplierPerBlock); - expect(await jumpRateModel.kink()).equal(kink); - - await jumpRateModel.updateJumpRateModel(convertToUnit(3, 12), convertToUnit(5, 14), convertToUnit(2.2, 18), kink); - - baseRatePerBlock = new BigNumber(convertToUnit(3, 12)).dividedBy(blocksPerYear).toFixed(0); - multiplierPerBlock = new BigNumber(convertToUnit(5, 14)).dividedBy(new BigNumber(blocksPerYear)).toFixed(0); - jumpMultiplierPerBlock = new BigNumber(convertToUnit(2.2, 18)).dividedBy(blocksPerYear).toFixed(0); - - expect(await jumpRateModel.baseRatePerBlock()).equal(baseRatePerBlock); - expect(await jumpRateModel.multiplierPerBlock()).equal(multiplierPerBlock); - expect(await jumpRateModel.jumpMultiplierPerBlock()).equal(jumpMultiplierPerBlock); - expect(await jumpRateModel.kink()).equal(kink); - }); - - it("Utilization rate: borrows and badDebt is zero", async () => { - expect(await jumpRateModel.utilizationRate(cash, 0, reserves, 0)).equal(0); - }); - - it("Utilization rate", async () => { - const utilizationRate = new BigNumber(Number(borrows) + Number(badDebt)) - .multipliedBy(expScale) - .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)) - .toFixed(0); - - expect(await jumpRateModel.utilizationRate(cash, borrows, reserves, badDebt)).equal(utilizationRate); - }); - - it("Borrow Rate: below kink utilization", async () => { - const multiplierPerBlock = (await jumpRateModel.multiplierPerBlock()).toString(); - const baseRatePerBlock = (await jumpRateModel.baseRatePerBlock()).toString(); - const utilizationRate = (await jumpRateModel.utilizationRate(cash, borrows, reserves, badDebt)).toString(); - - const value = new BigNumber(utilizationRate).multipliedBy(multiplierPerBlock).dividedBy(expScale).toFixed(0); - - expect(await jumpRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).equal( - Number(value) + Number(baseRatePerBlock), - ); - }); - - it("Borrow Rate: above kink utilization", async () => { - const multiplierPerBlock = (await jumpRateModel.multiplierPerBlock()).toString(); - const jumpMultiplierPerBlock = (await jumpRateModel.jumpMultiplierPerBlock()).toString(); - const baseRatePerBlock = (await jumpRateModel.baseRatePerBlock()).toString(); - const utilizationRate = ( - await jumpRateModel.utilizationRate(convertToUnit(6, 19), convertToUnit(16, 19), reserves, badDebt) - ).toString(); - - const value = new BigNumber(kink).multipliedBy(multiplierPerBlock).dividedBy(expScale).toFixed(0); - - const normalRate = Number(value) + Number(baseRatePerBlock); - const excessUtil = Number(utilizationRate) - Number(kink); - - const jumpValue = new BigNumber(excessUtil).multipliedBy(jumpMultiplierPerBlock).dividedBy(expScale).toFixed(0); - - expect(await jumpRateModel.getBorrowRate(convertToUnit(6, 19), convertToUnit(16, 19), reserves, badDebt)).equal( - Number(jumpValue) + Number(normalRate), - ); - }); - - it("Supply Rate", async () => { - const reserveMantissa = convertToUnit(1, 17); - const oneMinusReserveFactor = Number(expScale) - Number(reserveMantissa); - const borrowRate = (await jumpRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).toString(); - const rateToPool = new BigNumber(borrowRate).multipliedBy(oneMinusReserveFactor).dividedBy(expScale).toFixed(0); - const rate = new BigNumber(borrows) - .multipliedBy(expScale) - .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)); - const supplyRate = new BigNumber(rateToPool).multipliedBy(rate).dividedBy(expScale).toFixed(0); - - expect(await jumpRateModel.getSupplyRate(cash, borrows, reserves, reserveMantissa, badDebt)).equal(supplyRate); - }); -}); +} diff --git a/tests/hardhat/Lens/PoolLens.ts b/tests/hardhat/Lens/PoolLens.ts index 95f996f03..7e8a528f0 100644 --- a/tests/hardhat/Lens/PoolLens.ts +++ b/tests/hardhat/Lens/PoolLens.ts @@ -5,7 +5,7 @@ import { BigNumberish, Signer } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; -import { blocksPerYear } from "../../../helpers/deploymentConfig"; +import { BSC_BLOCKS_PER_YEAR } from "../../../helpers/deploymentConfig"; import { AccessControlManager, Beacon, @@ -21,6 +21,7 @@ import { WhitePaperInterestRateModel__factory, } from "../../../typechain"; import { makeVToken } from "../util/TokenTestHelpers"; +import { getDescription } from "../util/descriptionHelpers"; // Disable a warning about mixing beacons and transparent proxies upgrades.silenceWarnings(); @@ -45,406 +46,412 @@ const assertVTokenMetadata = (vTokenMetadataActual: any, vTokenMetadataExpected: }); }; -describe("PoolLens", async function () { - let poolRegistry: PoolRegistry; - let poolRegistryAddress: string; - let vTokenBeacon: Beacon; - let mockDAI: MockToken; - let mockWBTC: MockToken; - let vDAI: VToken; - let vWBTC: VToken; - let priceOracle: MockPriceOracle; - let comptroller1Proxy: Comptroller; - let comptroller2Proxy: Comptroller; - let poolLens: PoolLens; - let ownerAddress: string; - let fakeAccessControlManager: FakeContract; - let closeFactor1: BigNumberish; - let closeFactor2: BigNumberish; - let liquidationIncentive1: BigNumberish; - let liquidationIncentive2: BigNumberish; - const minLiquidatableCollateral = parseUnits("100", 18); - const maxLoopsLimit = 150; - const defaultBtcPrice = "21000.34"; - const defaultDaiPrice = "1"; - let borrowerWbtc: Signer; - let borrowerDai: Signer; - - interface PoolRegistryFixture { - poolRegistry: PoolRegistry; - fakeAccessControlManager: FakeContract; - } - - const poolRegistryFixture = async (): Promise => { - fakeAccessControlManager = await smock.fake("AccessControlManager"); - fakeAccessControlManager.isAllowedToCall.returns(true); - - const PoolRegistry = await ethers.getContractFactory("PoolRegistry"); - poolRegistry = (await upgrades.deployProxy(PoolRegistry, [fakeAccessControlManager.address])) as PoolRegistry; - - return { poolRegistry, fakeAccessControlManager }; - }; - - const [ACTION_LIQUIDATE, ACTION_EXIT_MARKET] = [5, 8]; - - /** - * Deploying required contracts along with the poolRegistry. - */ - before(async function () { - const [owner, ...rest] = await ethers.getSigners(); - [borrowerWbtc, borrowerDai] = rest; - ownerAddress = await owner.getAddress(); - - ({ poolRegistry, fakeAccessControlManager } = await loadFixture(poolRegistryFixture)); - poolRegistryAddress = poolRegistry.address; - - const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); - priceOracle = await MockPriceOracle.deploy(); - - closeFactor1 = parseUnits("0.05", 18); - liquidationIncentive1 = parseUnits("1", 18); - - const Comptroller = await ethers.getContractFactory("Comptroller"); - const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - - [comptroller1Proxy, comptroller2Proxy] = await Promise.all( - [...Array(3)].map(async () => { - const comptroller = await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ - maxLoopsLimit, - fakeAccessControlManager.address, - ]); - await comptroller.setPriceOracle(priceOracle.address); - return comptroller as Comptroller; - }), - ); - - // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptroller1Proxy.address, - closeFactor1, - liquidationIncentive1, - minLiquidatableCollateral, - ); - - closeFactor2 = parseUnits("0.05", 18); - liquidationIncentive2 = parseUnits("1", 18); - - // Registering the second pool - await poolRegistry.addPool( - "Pool 2", - comptroller2Proxy.address, - closeFactor2, - liquidationIncentive2, - minLiquidatableCollateral, - ); - - const MockToken = await ethers.getContractFactory("MockToken"); - mockDAI = await MockToken.deploy("MakerDAO", "DAI", 18); - await mockDAI.faucet(parseUnits("1000", 18)); - - mockWBTC = await MockToken.deploy("Bitcoin", "BTC", 8); - await mockWBTC.faucet(parseUnits("1000", 8)); - - await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); - await priceOracle.setPrice(mockWBTC.address, parseUnits(defaultBtcPrice, 28)); - - // Get all pools list. - const pools = await poolRegistry.callStatic.getAllPools(); - expect(pools[0].name).equal("Pool 1"); - expect(pools[1].name).equal("Pool 2"); - - const initialSupply = parseUnits("1000", 18); - const RateModel = await ethers.getContractFactory( - "WhitePaperInterestRateModel", - ); - const whitePaperInterestRateModel = await RateModel.deploy(blocksPerYear.hardhat, 0, parseUnits("0.04", 18)); - vWBTC = await makeVToken({ - underlying: mockWBTC, - comptroller: comptroller1Proxy, - accessControlManager: fakeAccessControlManager, - decimals: 8, - initialExchangeRateMantissa: parseUnits("1", 18), - admin: owner, - interestRateModel: whitePaperInterestRateModel, - beacon: vTokenBeacon, - }); - - await mockWBTC.faucet(initialSupply); - await mockWBTC.approve(poolRegistry.address, initialSupply); - await poolRegistry.addMarket({ - vToken: vWBTC.address, - collateralFactor: parseUnits("0.7", 18), - liquidationThreshold: parseUnits("0.7", 18), - initialSupply, - vTokenReceiver: owner.address, - supplyCap: parseUnits("4000", 18), - borrowCap: parseUnits("2000", 18), - }); - - vDAI = await makeVToken({ - underlying: mockDAI, - comptroller: comptroller1Proxy, - accessControlManager: fakeAccessControlManager, - decimals: 18, - initialExchangeRateMantissa: parseUnits("1", 18), - admin: owner, - interestRateModel: whitePaperInterestRateModel, - beacon: vTokenBeacon, - }); - - await mockDAI.faucet(initialSupply); - await mockDAI.approve(poolRegistry.address, initialSupply); - await poolRegistry.addMarket({ - vToken: vDAI.address, - collateralFactor: parseUnits("0.7", 18), - liquidationThreshold: parseUnits("0.7", 18), - initialSupply, - vTokenReceiver: owner.address, - supplyCap: initialSupply, - borrowCap: initialSupply, - }); - - await poolRegistry.updatePoolMetadata(comptroller1Proxy.address, { - category: "High market cap", - logoURL: "http://venis.io/pool1", - description: "Pool1 description", - }); - - await poolRegistry.updatePoolMetadata(comptroller2Proxy.address, { - category: "Low market cap", - logoURL: "http://highrisk.io/pool2", - description: "Pool2 description", - }); - - const vWBTCAddress = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vDAIAddress = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - - vWBTC = await ethers.getContractAt("VToken", vWBTCAddress); - vDAI = await ethers.getContractAt("VToken", vDAIAddress); - - // Enter Markets - await comptroller1Proxy.enterMarkets([vDAI.address, vWBTC.address]); - await comptroller1Proxy.connect(owner).enterMarkets([vDAI.address, vWBTC.address]); - - // Pause some actions - await comptroller1Proxy - .connect(owner) - .setActionsPaused([vWBTC.address], [ACTION_LIQUIDATE, ACTION_EXIT_MARKET], true); - - const PoolLens = await ethers.getContractFactory("PoolLens"); - poolLens = await PoolLens.deploy(); - }); - - describe("PoolView Tests", () => { - it("get All Pools", async function () { - const poolData = await poolLens.getAllPools(poolRegistryAddress); - - expect(poolData.length).equal(2); - - const venusPool1Actual = poolData[0]; - - expect(venusPool1Actual.name).equal("Pool 1"); - expect(venusPool1Actual.creator).equal(ownerAddress); - expect(venusPool1Actual.comptroller).equal(comptroller1Proxy.address); - expect(venusPool1Actual.category).equal("High market cap"); - expect(venusPool1Actual.logoURL).equal("http://venis.io/pool1"); - expect(venusPool1Actual.description).equal("Pool1 description"); - expect(venusPool1Actual.priceOracle).equal(priceOracle.address); - expect(venusPool1Actual.closeFactor).equal(closeFactor1); - expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive1); - expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); - - const vTokensActual = venusPool1Actual.vTokens; - expect(vTokensActual.length).equal(2); - - // get VToken for Asset-1 : WBTC - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenMetadataWBTCExpected = await poolLens.vTokenMetadata(vTokenAddressWBTC); - const vTokenMetadataWBTCActual = vTokensActual[0]; - assertVTokenMetadata(vTokenMetadataWBTCActual, vTokenMetadataWBTCExpected); - - // get VToken for Asset-2 : DAI - const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - const vTokenMetadataDAIExpected = await poolLens.vTokenMetadata(vTokenAddressDAI); - const vTokenMetadataDAIActual = vTokensActual[1]; - assertVTokenMetadata(vTokenMetadataDAIActual, vTokenMetadataDAIExpected); - - const venusPool2Actual = poolData[1]; - - expect(venusPool2Actual.name).equal("Pool 2"); - expect(venusPool2Actual.creator).equal(ownerAddress); - expect(venusPool2Actual.comptroller).equal(comptroller2Proxy.address); - expect(venusPool2Actual.category).equal("Low market cap"); - expect(venusPool2Actual.logoURL).equal("http://highrisk.io/pool2"); - expect(venusPool2Actual.description).equal("Pool2 description"); - expect(venusPool1Actual.priceOracle).equal(priceOracle.address); - expect(venusPool1Actual.closeFactor).equal(closeFactor2); - expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive2); - expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); - }); - - it("getPoolData By Comptroller", async function () { - const poolData = await poolLens.getPoolByComptroller(poolRegistryAddress, comptroller1Proxy.address); - - expect(poolData.name).equal("Pool 1"); - expect(poolData.creator).equal(ownerAddress); - expect(poolData.comptroller).equal(comptroller1Proxy.address); - expect(poolData.category).equal("High market cap"); - expect(poolData.logoURL).equal("http://venis.io/pool1"); - expect(poolData.description).equal("Pool1 description"); - expect(poolData.priceOracle).equal(priceOracle.address); - expect(poolData.closeFactor).equal(closeFactor1); - expect(poolData.liquidationIncentive).equal(liquidationIncentive1); - expect(poolData.minLiquidatableCollateral).equal(minLiquidatableCollateral); - - const vTokensActual = poolData.vTokens; - expect(vTokensActual.length).equal(2); - - // get VToken for Asset-1 : WBTC - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenMetadataWBTCExpected = await poolLens.vTokenMetadata(vTokenAddressWBTC); - const vTokenMetadataWBTCActual = vTokensActual[0]; - assertVTokenMetadata(vTokenMetadataWBTCActual, vTokenMetadataWBTCExpected); - - // get VToken for Asset-2 : DAI - const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - const vTokenMetadataDAIExpected = await poolLens.vTokenMetadata(vTokenAddressDAI); - const vTokenMetadataDAIActual = vTokensActual[1]; - assertVTokenMetadata(vTokenMetadataDAIActual, vTokenMetadataDAIExpected); - }); - }); - - describe("PoolLens - VTokens Query Tests", async function () { - it("get all info of pools user specific", async function () { - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - const res = await poolLens.callStatic.vTokenBalancesAll([vTokenAddressWBTC, vTokenAddressDAI], ownerAddress); - expect(res[0][0]).equal(vTokenAddressWBTC); - expect(res[1][0]).equal(vTokenAddressDAI); - }); - - it("get underlying price", async function () { - const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - const res = await poolLens.vTokenUnderlyingPrice(vTokenAddressDAI); - expect(res[1]).equal(parseUnits("1", 18)); - }); +for (const isTimeBased of [false, true]) { + const description = getDescription(isTimeBased); + const slotsPerYear = isTimeBased ? 0 : BSC_BLOCKS_PER_YEAR; + + describe(`${description}PoolLens`, async () => { + let poolRegistry: PoolRegistry; + let poolRegistryAddress: string; + let vTokenBeacon: Beacon; + let mockDAI: MockToken; + let mockWBTC: MockToken; + let vDAI: VToken; + let vWBTC: VToken; + let priceOracle: MockPriceOracle; + let comptroller1Proxy: Comptroller; + let comptroller2Proxy: Comptroller; + let poolLens: PoolLens; + let ownerAddress: string; + let fakeAccessControlManager: FakeContract; + let closeFactor1: BigNumberish; + let closeFactor2: BigNumberish; + let liquidationIncentive1: BigNumberish; + let liquidationIncentive2: BigNumberish; + const minLiquidatableCollateral = parseUnits("100", 18); + const maxLoopsLimit = 150; + const defaultBtcPrice = "21000.34"; + const defaultDaiPrice = "1"; + let borrowerWbtc: Signer; + let borrowerDai: Signer; + + interface PoolRegistryFixture { + poolRegistry: PoolRegistry; + fakeAccessControlManager: FakeContract; + } - it("get underlying price all", async function () { - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); - const res = await poolLens.vTokenUnderlyingPriceAll([vTokenAddressDAI, vTokenAddressWBTC]); - expect(res[0][1]).equal(parseUnits("1", 18)); - expect(res[1][1]).equal(parseUnits("21000.34", 28)); - }); + const poolRegistryFixture = async (): Promise => { + fakeAccessControlManager = await smock.fake("AccessControlManager"); + fakeAccessControlManager.isAllowedToCall.returns(true); + + const PoolRegistry = await ethers.getContractFactory("PoolRegistry"); + poolRegistry = (await upgrades.deployProxy(PoolRegistry, [fakeAccessControlManager.address])) as PoolRegistry; + + return { poolRegistry, fakeAccessControlManager }; + }; + + const [ACTION_LIQUIDATE, ACTION_EXIT_MARKET] = [5, 8]; + + /** + * Deploying required contracts along with the poolRegistry. + */ + before(async () => { + const [owner, ...rest] = await ethers.getSigners(); + [borrowerWbtc, borrowerDai] = rest; + ownerAddress = await owner.getAddress(); + + ({ poolRegistry, fakeAccessControlManager } = await loadFixture(poolRegistryFixture)); + poolRegistryAddress = poolRegistry.address; + const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); + priceOracle = await MockPriceOracle.deploy(); + + closeFactor1 = parseUnits("0.05", 18); + liquidationIncentive1 = parseUnits("1", 18); + + const Comptroller = await ethers.getContractFactory("Comptroller"); + const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + + [comptroller1Proxy, comptroller2Proxy] = await Promise.all( + [...Array(3)].map(async () => { + const comptroller = await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ + maxLoopsLimit, + fakeAccessControlManager.address, + ]); + await comptroller.setPriceOracle(priceOracle.address); + return comptroller as Comptroller; + }), + ); - it("get underlying price all", async function () { - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenAddressWBTCPool = await poolLens.getVTokenForAsset( - poolRegistry.address, + // Registering the first pool + await poolRegistry.addPool( + "Pool 1", comptroller1Proxy.address, - mockWBTC.address, + closeFactor1, + liquidationIncentive1, + minLiquidatableCollateral, ); - expect(vTokenAddressWBTC).equal(vTokenAddressWBTCPool); - }); - - it("is correct for WBTC as underlyingAsset", async () => { - // get vToken for Asset-1 : WBTC - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenMetadataActual = await poolLens.vTokenMetadata(vTokenAddressWBTC); - - const vTokenMetadataActualParsed = cullTuple(vTokenMetadataActual); - expect(vTokenMetadataActualParsed["vToken"]).equal(vTokenAddressWBTC); - expect(vTokenMetadataActualParsed["exchangeRateCurrent"]).equal(parseUnits("1", 18)); - expect(vTokenMetadataActualParsed["supplyRatePerBlock"]).equal("0"); - expect(vTokenMetadataActualParsed["borrowRatePerBlock"]).equal("0"); - expect(vTokenMetadataActualParsed["reserveFactorMantissa"]).equal(parseUnits("0.3", 18)); - expect(vTokenMetadataActualParsed["supplyCaps"]).equal("4000000000000000000000"); - expect(vTokenMetadataActualParsed["borrowCaps"]).equal("2000000000000000000000"); - expect(vTokenMetadataActualParsed["totalBorrows"]).equal("0"); - expect(vTokenMetadataActualParsed["totalReserves"]).equal("0"); - expect(vTokenMetadataActualParsed["totalSupply"]).equal(parseUnits("10000000000000", 8)); - expect(vTokenMetadataActualParsed["totalCash"]).equal(parseUnits("10000000000000", 8)); - expect(vTokenMetadataActualParsed["isListed"]).equal("true"); - expect(vTokenMetadataActualParsed["collateralFactorMantissa"]).equal("700000000000000000"); - expect(vTokenMetadataActualParsed["underlyingAssetAddress"]).equal(mockWBTC.address); - expect(vTokenMetadataActualParsed["vTokenDecimals"]).equal("8"); - expect(vTokenMetadataActualParsed["underlyingDecimals"]).equal("8"); - const expectedBitmask = (1 << ACTION_LIQUIDATE) | (1 << ACTION_EXIT_MARKET); - expect(Number(vTokenMetadataActualParsed["pausedActions"])).equal(expectedBitmask); - }); - - it("is correct minted for user", async () => { - const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); - const vTokenBalance = await poolLens.callStatic.vTokenBalances(vTokenAddressWBTC, ownerAddress); - expect(vTokenBalance["balanceOfUnderlying"]).equal(parseUnits("10000000000000", 8)); - expect(vTokenBalance["balanceOf"]).equal(parseUnits("10000000000000", 8)); - }); - }); - - describe("Shortfall view functions", () => { - it("getPoolBadDebt - no pools have debt", async () => { - const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); - - expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); - expect(resp.totalBadDebtUsd).to.be.equal(0); - expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); - expect(resp.badDebts[0][1].toString()).to.be.equal("0"); + closeFactor2 = parseUnits("0.05", 18); + liquidationIncentive2 = parseUnits("1", 18); - expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); - expect(resp.badDebts[1][1].toString()).to.be.equal("0"); - }); - - it("getPoolBadDebt - all pools have debt", async () => { - const [owner] = await ethers.getSigners(); - // Setup - await comptroller1Proxy.setMarketSupplyCaps( - [vWBTC.address, vDAI.address], - [parseUnits("9000000000", 18), parseUnits("9000000000", 18)], + // Registering the second pool + await poolRegistry.addPool( + "Pool 2", + comptroller2Proxy.address, + closeFactor2, + liquidationIncentive2, + minLiquidatableCollateral, ); + const MockToken = await ethers.getContractFactory("MockToken"); + mockDAI = await MockToken.deploy("MakerDAO", "DAI", 18); + await mockDAI.faucet(parseUnits("1000", 18)); - await mockWBTC.connect(borrowerDai).faucet(parseUnits("20", 18)); - await mockWBTC.connect(borrowerDai).approve(vWBTC.address, parseUnits("20", 18)); - await vWBTC.connect(borrowerDai).mint(parseUnits("2", 18)); - await comptroller1Proxy.connect(borrowerDai).enterMarkets([vWBTC.address]); - await vDAI.connect(borrowerDai).borrow(parseUnits("0.05", 18)); - await mine(1); - - await mockDAI.connect(borrowerWbtc).faucet(parseUnits("900000000", 18)); - await mockDAI.connect(borrowerWbtc).approve(vDAI.address, parseUnits("9000000000", 18)); - await mockDAI.connect(borrowerWbtc).approve(vDAI.address, parseUnits("9000000000", 18)); - await vDAI.connect(borrowerWbtc).mint(parseUnits("900000000", 18)); - await comptroller1Proxy.connect(borrowerWbtc).enterMarkets([vDAI.address]); - await vWBTC.connect(borrowerWbtc).borrow(parseUnits("0.000001", 18)); - await mine(1); - - await mockDAI.connect(owner).approve(vDAI.address, parseUnits("9000000000", 18)); - await mockWBTC.connect(owner).approve(vWBTC.address, parseUnits("9000000000", 18)); - await priceOracle.setPrice(mockWBTC.address, parseUnits("0.000000000000001", 18)); - await mine(1); - - await comptroller1Proxy.connect(owner).healAccount(await borrowerDai.getAddress()); + mockWBTC = await MockToken.deploy("Bitcoin", "BTC", 8); + await mockWBTC.faucet(parseUnits("1000", 8)); + await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); await priceOracle.setPrice(mockWBTC.address, parseUnits(defaultBtcPrice, 28)); - await priceOracle.setPrice(mockDAI.address, parseUnits("0.0000000000000001", 18)); - await comptroller1Proxy.connect(owner).healAccount(await borrowerWbtc.getAddress()); - // End Setup - const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); + // Get all pools list. + const pools = await poolRegistry.callStatic.getAllPools(); + expect(pools[0].name).equal("Pool 1"); + expect(pools[1].name).equal("Pool 2"); - expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); - expect(resp.totalBadDebtUsd).to.be.equal("210003400000000000000000005"); + const initialSupply = parseUnits("1000", 18); + const RateModel = await ethers.getContractFactory( + "WhitePaperInterestRateModel", + ); + const whitePaperInterestRateModel = await RateModel.deploy(0, parseUnits("0.04", 18), isTimeBased, slotsPerYear); + vWBTC = await makeVToken({ + underlying: mockWBTC, + comptroller: comptroller1Proxy, + accessControlManager: fakeAccessControlManager, + decimals: 8, + initialExchangeRateMantissa: parseUnits("1", 18), + admin: owner, + interestRateModel: whitePaperInterestRateModel, + beacon: vTokenBeacon, + isTimeBased: isTimeBased, + blocksPerYear: slotsPerYear, + }); + + vDAI = await makeVToken({ + underlying: mockDAI, + comptroller: comptroller1Proxy, + accessControlManager: fakeAccessControlManager, + decimals: 18, + initialExchangeRateMantissa: parseUnits("1", 18), + admin: owner, + interestRateModel: whitePaperInterestRateModel, + beacon: vTokenBeacon, + isTimeBased: isTimeBased, + blocksPerYear: slotsPerYear, + }); + + await mockWBTC.faucet(initialSupply); + await mockWBTC.approve(poolRegistry.address, initialSupply); + await poolRegistry.addMarket({ + vToken: vWBTC.address, + collateralFactor: parseUnits("0.7", 18), + liquidationThreshold: parseUnits("0.7", 18), + initialSupply, + vTokenReceiver: owner.address, + supplyCap: parseUnits("4000", 18), + borrowCap: parseUnits("2000", 18), + }); + + await mockDAI.faucet(initialSupply); + await mockDAI.approve(poolRegistry.address, initialSupply); + await poolRegistry.addMarket({ + vToken: vDAI.address, + collateralFactor: parseUnits("0.7", 18), + liquidationThreshold: parseUnits("0.7", 18), + initialSupply, + vTokenReceiver: owner.address, + supplyCap: initialSupply, + borrowCap: initialSupply, + }); + + await poolRegistry.updatePoolMetadata(comptroller1Proxy.address, { + category: "High market cap", + logoURL: "http://venis.io/pool1", + description: "Pool1 description", + }); + + await poolRegistry.updatePoolMetadata(comptroller2Proxy.address, { + category: "Low market cap", + logoURL: "http://highrisk.io/pool2", + description: "Pool2 description", + }); + + const vWBTCAddress = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vDAIAddress = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + vWBTC = await ethers.getContractAt("VToken", vWBTCAddress); + vDAI = await ethers.getContractAt("VToken", vDAIAddress); + + // Enter Markets + await comptroller1Proxy.enterMarkets([vDAI.address, vWBTC.address]); + await comptroller1Proxy.connect(owner).enterMarkets([vDAI.address, vWBTC.address]); + + // Pause some actions + await comptroller1Proxy + .connect(owner) + .setActionsPaused([vWBTC.address], [ACTION_LIQUIDATE, ACTION_EXIT_MARKET], true); + + const PoolLens = await ethers.getContractFactory("PoolLens"); + poolLens = await PoolLens.deploy(isTimeBased, slotsPerYear); + }); - expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); - expect(resp.badDebts[1][1].toString()).to.be.equal("5"); + describe(`${description}PoolView Tests`, () => { + it("get All Pools", async function () { + const poolData = await poolLens.getAllPools(poolRegistryAddress); + + expect(poolData.length).equal(2); + + const venusPool1Actual = poolData[0]; + + expect(venusPool1Actual.name).equal("Pool 1"); + expect(venusPool1Actual.creator).equal(ownerAddress); + expect(venusPool1Actual.comptroller).equal(comptroller1Proxy.address); + expect(venusPool1Actual.category).equal("High market cap"); + expect(venusPool1Actual.logoURL).equal("http://venis.io/pool1"); + expect(venusPool1Actual.description).equal("Pool1 description"); + expect(venusPool1Actual.priceOracle).equal(priceOracle.address); + expect(venusPool1Actual.closeFactor).equal(closeFactor1); + expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive1); + expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); + + const vTokensActual = venusPool1Actual.vTokens; + expect(vTokensActual.length).equal(2); + + // get VToken for Asset-1 : WBTC + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenMetadataWBTCExpected = await poolLens.vTokenMetadata(vTokenAddressWBTC); + const vTokenMetadataWBTCActual = vTokensActual[0]; + assertVTokenMetadata(vTokenMetadataWBTCActual, vTokenMetadataWBTCExpected); + + // get VToken for Asset-2 : DAI + const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + const vTokenMetadataDAIExpected = await poolLens.vTokenMetadata(vTokenAddressDAI); + const vTokenMetadataDAIActual = vTokensActual[1]; + assertVTokenMetadata(vTokenMetadataDAIActual, vTokenMetadataDAIExpected); + + const venusPool2Actual = poolData[1]; + + expect(venusPool2Actual.name).equal("Pool 2"); + expect(venusPool2Actual.creator).equal(ownerAddress); + expect(venusPool2Actual.comptroller).equal(comptroller2Proxy.address); + expect(venusPool2Actual.category).equal("Low market cap"); + expect(venusPool2Actual.logoURL).equal("http://highrisk.io/pool2"); + expect(venusPool2Actual.description).equal("Pool2 description"); + expect(venusPool1Actual.priceOracle).equal(priceOracle.address); + expect(venusPool1Actual.closeFactor).equal(closeFactor2); + expect(venusPool1Actual.liquidationIncentive).equal(liquidationIncentive2); + expect(venusPool1Actual.minLiquidatableCollateral).equal(minLiquidatableCollateral); + }); + + it("getPoolData By Comptroller", async function () { + const poolData = await poolLens.getPoolByComptroller(poolRegistryAddress, comptroller1Proxy.address); + + expect(poolData.name).equal("Pool 1"); + expect(poolData.creator).equal(ownerAddress); + expect(poolData.comptroller).equal(comptroller1Proxy.address); + expect(poolData.category).equal("High market cap"); + expect(poolData.logoURL).equal("http://venis.io/pool1"); + expect(poolData.description).equal("Pool1 description"); + expect(poolData.priceOracle).equal(priceOracle.address); + expect(poolData.closeFactor).equal(closeFactor1); + expect(poolData.liquidationIncentive).equal(liquidationIncentive1); + expect(poolData.minLiquidatableCollateral).equal(minLiquidatableCollateral); + + const vTokensActual = poolData.vTokens; + expect(vTokensActual.length).equal(2); + + // get VToken for Asset-1 : WBTC + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenMetadataWBTCExpected = await poolLens.vTokenMetadata(vTokenAddressWBTC); + const vTokenMetadataWBTCActual = vTokensActual[0]; + assertVTokenMetadata(vTokenMetadataWBTCActual, vTokenMetadataWBTCExpected); + + // get VToken for Asset-2 : DAI + const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + const vTokenMetadataDAIExpected = await poolLens.vTokenMetadata(vTokenAddressDAI); + const vTokenMetadataDAIActual = vTokensActual[1]; + assertVTokenMetadata(vTokenMetadataDAIActual, vTokenMetadataDAIExpected); + }); + }); - expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); - expect(resp.badDebts[0][1].toString()).to.be.equal("210003400000000000000000000"); + describe(`${description}PoolLens - VTokens Query Tests`, async function () { + it("get all info of pools user specific", async function () { + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + const res = await poolLens.callStatic.vTokenBalancesAll([vTokenAddressWBTC, vTokenAddressDAI], ownerAddress); + expect(res[0][0]).equal(vTokenAddressWBTC); + expect(res[1][0]).equal(vTokenAddressDAI); + }); + + it("get underlying price", async function () { + const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + const res = await poolLens.vTokenUnderlyingPrice(vTokenAddressDAI); + expect(res[1]).equal(parseUnits("1", 18)); + }); + + it("get underlying price all", async function () { + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenAddressDAI = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockDAI.address); + const res = await poolLens.vTokenUnderlyingPriceAll([vTokenAddressDAI, vTokenAddressWBTC]); + expect(res[0][1]).equal(parseUnits("1", 18)); + expect(res[1][1]).equal(parseUnits("21000.34", 28)); + }); + + it("get underlying price all", async function () { + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenAddressWBTCPool = await poolLens.getVTokenForAsset( + poolRegistry.address, + comptroller1Proxy.address, + mockWBTC.address, + ); + expect(vTokenAddressWBTC).equal(vTokenAddressWBTCPool); + }); + + it("is correct for WBTC as underlyingAsset", async () => { + // get vToken for Asset-1 : WBTC + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenMetadataActual = await poolLens.vTokenMetadata(vTokenAddressWBTC); + + const vTokenMetadataActualParsed = cullTuple(vTokenMetadataActual); + expect(vTokenMetadataActualParsed["vToken"]).equal(vTokenAddressWBTC); + expect(vTokenMetadataActualParsed["exchangeRateCurrent"]).equal(parseUnits("1", 18)); + expect(vTokenMetadataActualParsed["supplyRatePerBlockOrTimestamp"]).equal("0"); + expect(vTokenMetadataActualParsed["borrowRatePerBlockOrTimestamp"]).equal("0"); + expect(vTokenMetadataActualParsed["reserveFactorMantissa"]).equal(parseUnits("0.3", 18)); + expect(vTokenMetadataActualParsed["supplyCaps"]).equal("4000000000000000000000"); + expect(vTokenMetadataActualParsed["borrowCaps"]).equal("2000000000000000000000"); + expect(vTokenMetadataActualParsed["totalBorrows"]).equal("0"); + expect(vTokenMetadataActualParsed["totalReserves"]).equal("0"); + expect(vTokenMetadataActualParsed["totalSupply"]).equal(parseUnits("10000000000000", 8)); + expect(vTokenMetadataActualParsed["totalCash"]).equal(parseUnits("10000000000000", 8)); + expect(vTokenMetadataActualParsed["isListed"]).equal("true"); + expect(vTokenMetadataActualParsed["collateralFactorMantissa"]).equal("700000000000000000"); + expect(vTokenMetadataActualParsed["underlyingAssetAddress"]).equal(mockWBTC.address); + expect(vTokenMetadataActualParsed["vTokenDecimals"]).equal("8"); + expect(vTokenMetadataActualParsed["underlyingDecimals"]).equal("8"); + const expectedBitmask = (1 << ACTION_LIQUIDATE) | (1 << ACTION_EXIT_MARKET); + expect(Number(vTokenMetadataActualParsed["pausedActions"])).equal(expectedBitmask); + }); + + it("is correct minted for user", async () => { + const vTokenAddressWBTC = await poolRegistry.getVTokenForAsset(comptroller1Proxy.address, mockWBTC.address); + const vTokenBalance = await poolLens.callStatic.vTokenBalances(vTokenAddressWBTC, ownerAddress); + expect(vTokenBalance["balanceOfUnderlying"]).equal(parseUnits("10000000000000", 8)); + expect(vTokenBalance["balanceOf"]).equal(parseUnits("10000000000000", 8)); + }); + }); - // Cleanup - await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); - await priceOracle.setPrice(mockWBTC.address, parseUnits(defaultBtcPrice, 28)); + describe("Shortfall view functions", () => { + it("getPoolBadDebt - no pools have debt", async () => { + const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); + + expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); + expect(resp.totalBadDebtUsd).to.be.equal(0); + + expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); + expect(resp.badDebts[0][1].toString()).to.be.equal("0"); + + expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); + expect(resp.badDebts[1][1].toString()).to.be.equal("0"); + }); + + it("getPoolBadDebt - all pools have debt", async () => { + const [owner] = await ethers.getSigners(); + // Setup + await comptroller1Proxy.setMarketSupplyCaps( + [vWBTC.address, vDAI.address], + [parseUnits("9000000000", 18), parseUnits("9000000000", 18)], + ); + + await mockWBTC.connect(borrowerDai).faucet(parseUnits("20", 18)); + await mockWBTC.connect(borrowerDai).approve(vWBTC.address, parseUnits("20", 18)); + await vWBTC.connect(borrowerDai).mint(parseUnits("2", 18)); + await comptroller1Proxy.connect(borrowerDai).enterMarkets([vWBTC.address]); + await vDAI.connect(borrowerDai).borrow(parseUnits("0.05", 18)); + await mine(1); + + await mockDAI.connect(borrowerWbtc).faucet(parseUnits("900000000", 18)); + await mockDAI.connect(borrowerWbtc).approve(vDAI.address, parseUnits("9000000000", 18)); + await mockDAI.connect(borrowerWbtc).approve(vDAI.address, parseUnits("9000000000", 18)); + await vDAI.connect(borrowerWbtc).mint(parseUnits("900000000", 18)); + await comptroller1Proxy.connect(borrowerWbtc).enterMarkets([vDAI.address]); + await vWBTC.connect(borrowerWbtc).borrow(parseUnits("0.000001", 18)); + await mine(1); + + await mockDAI.connect(owner).approve(vDAI.address, parseUnits("9000000000", 18)); + await mockWBTC.connect(owner).approve(vWBTC.address, parseUnits("9000000000", 18)); + await priceOracle.setPrice(mockWBTC.address, parseUnits("0.000000000000001", 18)); + await mine(1); + + await comptroller1Proxy.connect(owner).healAccount(await borrowerDai.getAddress()); + + await priceOracle.setPrice(mockWBTC.address, parseUnits(defaultBtcPrice, 28)); + await priceOracle.setPrice(mockDAI.address, parseUnits("0.0000000000000001", 18)); + await comptroller1Proxy.connect(owner).healAccount(await borrowerWbtc.getAddress()); + // End Setup + + const resp = await poolLens.getPoolBadDebt(comptroller1Proxy.address); + + expect(resp.comptroller).to.be.equal(comptroller1Proxy.address); + expect(resp.totalBadDebtUsd).to.be.equal("210003400000000000000000005"); + + expect(resp.badDebts[1][0]).to.be.equal(vDAI.address); + expect(resp.badDebts[1][1].toString()).to.be.equal("5"); + + expect(resp.badDebts[0][0]).to.be.equal(vWBTC.address); + expect(resp.badDebts[0][1].toString()).to.be.equal("210003400000000000000000000"); + + // Cleanup + await priceOracle.setPrice(mockDAI.address, parseUnits(defaultDaiPrice, 18)); + await priceOracle.setPrice(mockWBTC.address, parseUnits(defaultBtcPrice, 28)); + }); }); }); -}); +} diff --git a/tests/hardhat/Lens/RewardsSummary.ts b/tests/hardhat/Lens/RewardsSummary.ts index 6a72d37f0..b6eca1e25 100644 --- a/tests/hardhat/Lens/RewardsSummary.ts +++ b/tests/hardhat/Lens/RewardsSummary.ts @@ -4,8 +4,10 @@ import chai from "chai"; import { BigNumber, Signer } from "ethers"; import { ethers } from "hardhat"; +import { BSC_BLOCKS_PER_YEAR } from "../../../helpers/deploymentConfig"; import { convertToUnit } from "../../../helpers/utils"; import { Comptroller, MockToken, PoolLens, PoolLens__factory, RewardsDistributor, VToken } from "../../../typechain"; +import { getDescription } from "../util/descriptionHelpers"; const { expect } = chai; chai.use(smock.matchers); @@ -25,6 +27,8 @@ let rewardToken3: FakeContract; let poolLens: MockContract; let account: Signer; let startBlock: number; +let isTimeBased = false; // for block based contracts +let blocksPerYear = BSC_BLOCKS_PER_YEAR; // for block based contracts type RewardsFixtire = { comptroller: FakeContract; @@ -51,7 +55,7 @@ const rewardsFixture = async (): Promise => { rewardToken2 = await smock.fake("MockToken"); rewardToken3 = await smock.fake("MockToken"); const poolLensFactory = await smock.mock("PoolLens"); - poolLens = await poolLensFactory.deploy(); + poolLens = await poolLensFactory.deploy(isTimeBased, blocksPerYear); const startBlock = await ethers.provider.getBlockNumber(); @@ -63,6 +67,7 @@ const rewardsFixture = async (): Promise => { rewardDistributor3.address, ]); + rewardDistributor1.isTimeBased.returns(false); rewardDistributor1.rewardToken.returns(rewardToken1.address); rewardDistributor1.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); rewardDistributor1.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); @@ -82,6 +87,7 @@ const rewardsFixture = async (): Promise => { lastRewardingBlock: 0, }); + rewardDistributor2.isTimeBased.returns(false); rewardDistributor2.rewardToken.returns(rewardToken2.address); rewardDistributor2.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); rewardDistributor2.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); @@ -101,6 +107,7 @@ const rewardsFixture = async (): Promise => { lastRewardingBlock: 0, }); + rewardDistributor3.isTimeBased.returns(false); rewardDistributor3.rewardToken.returns(rewardToken3.address); rewardDistributor3.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); rewardDistributor3.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); @@ -147,124 +154,284 @@ const rewardsFixture = async (): Promise => { }; }; -describe("PoolLens: Rewards Summary", () => { - beforeEach(async () => { - [account] = await ethers.getSigners(); - ({ - comptroller, - vBUSD, - vWBTC, - rewardDistributor1, - rewardToken1, - rewardDistributor2, - rewardToken2, - rewardDistributor3, - rewardToken3, - poolLens, - startBlock, - } = await loadFixture(rewardsFixture)); +const timeBasedRewardsFixture = async (): Promise => { + comptroller = await smock.fake("Comptroller"); + vBUSD = await smock.fake("VToken"); + vWBTC = await smock.fake("VToken"); + rewardDistributor1 = await smock.fake("RewardsDistributor"); + rewardDistributor2 = await smock.fake("RewardsDistributor"); + rewardDistributor3 = await smock.fake("RewardsDistributor"); + rewardToken1 = await smock.fake("MockToken"); + rewardToken2 = await smock.fake("MockToken"); + rewardToken3 = await smock.fake("MockToken"); + const poolLensFactory = await smock.mock("PoolLens"); + + isTimeBased = true; + blocksPerYear = 0; + poolLens = await poolLensFactory.deploy(isTimeBased, blocksPerYear); + + const startBlock = (await ethers.provider.getBlock("latest")).number; + const startBlockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + + // Fake return values + comptroller.getAllMarkets.returns([vBUSD.address, vWBTC.address]); + comptroller.getRewardDistributors.returns([ + rewardDistributor1.address, + rewardDistributor2.address, + rewardDistributor3.address, + ]); + + rewardDistributor1.isTimeBased.returns(true); + rewardDistributor1.rewardToken.returns(rewardToken1.address); + rewardDistributor1.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); + rewardDistributor1.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); + rewardDistributor1.rewardTokenAccrued.returns(convertToUnit(50, 18)); + rewardDistributor1.rewardTokenSupplySpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor1.rewardTokenSupplySpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor1.rewardTokenBorrowSpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor1.rewardTokenBorrowSpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor1.rewardTokenBorrowStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, + }); + rewardDistributor1.rewardTokenSupplyStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, }); - it("Should get summary for all markets", async () => { - // Mine some blocks so deltaBlocks != 0 - await mineUpTo(startBlock + 1000); + rewardDistributor2.isTimeBased.returns(true); + rewardDistributor2.rewardToken.returns(rewardToken2.address); + rewardDistributor2.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); + rewardDistributor2.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); + rewardDistributor2.rewardTokenAccrued.returns(convertToUnit(50, 18)); + rewardDistributor2.rewardTokenSupplySpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor2.rewardTokenSupplySpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor2.rewardTokenBorrowSpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor2.rewardTokenBorrowSpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor2.rewardTokenBorrowStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, + }); + rewardDistributor2.rewardTokenSupplyStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, + }); - const accountAddress = await account.getAddress(); + rewardDistributor3.isTimeBased.returns(true); + rewardDistributor3.rewardToken.returns(rewardToken3.address); + rewardDistributor3.rewardTokenBorrowerIndex.returns(convertToUnit(1, 18)); + rewardDistributor3.rewardTokenSupplierIndex.returns(convertToUnit(1, 18)); + rewardDistributor3.rewardTokenAccrued.returns(convertToUnit(50, 18)); + rewardDistributor3.rewardTokenSupplySpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor3.rewardTokenSupplySpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor3.rewardTokenBorrowSpeeds.whenCalledWith(vBUSD.address).returns(convertToUnit(0.5, 18)); + rewardDistributor3.rewardTokenBorrowSpeeds.whenCalledWith(vWBTC.address).returns(convertToUnit(0.5, 18)); + rewardDistributor3.rewardTokenBorrowStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, + }); + rewardDistributor3.rewardTokenSupplyStateTimeBased.returns({ + index: convertToUnit(1, 18), + timestamp: startBlockTimestamp, + lastRewardingTimestamp: 0, + }); - const pendingRewards = await poolLens.getPendingRewards(accountAddress, comptroller.address); - expect(comptroller.getAllMarkets).to.have.been.calledOnce; - expect(comptroller.getRewardDistributors).to.have.been.calledOnce; + vBUSD.borrowIndex.returns(convertToUnit(1, 18)); + vBUSD.totalBorrows.returns(convertToUnit(10000, 8)); + vBUSD.totalSupply.returns(convertToUnit(10000, 8)); + vBUSD.balanceOf.returns(convertToUnit(100, 8)); + vBUSD.borrowBalanceStored.returns(convertToUnit(100, 8)); - expect(rewardDistributor1.rewardToken).to.have.been.calledOnce; - expect(rewardDistributor2.rewardToken).to.have.been.calledOnce; - expect(rewardDistributor3.rewardToken).to.have.been.calledOnce; + vWBTC.borrowIndex.returns(convertToUnit(1, 18)); + vWBTC.totalBorrows.returns(convertToUnit(100, 18)); + vWBTC.totalSupply.returns(convertToUnit(100, 18)); + vWBTC.balanceOf.returns(convertToUnit(100, 8)); + vWBTC.borrowBalanceStored.returns(convertToUnit(100, 8)); - expect(rewardDistributor1.rewardTokenAccrued).to.have.been.calledOnce; - expect(rewardDistributor2.rewardTokenAccrued).to.have.been.calledOnce; - expect(rewardDistributor3.rewardTokenAccrued).to.have.been.calledOnce; + return { + comptroller, + vBUSD, + vWBTC, + rewardDistributor1, + rewardToken1, + rewardDistributor2, + rewardToken2, + rewardDistributor3, + rewardToken3, + poolLens, + startBlock, + }; +}; + +async function setup(isTimeBased: boolean) { + if (!isTimeBased) return await loadFixture(rewardsFixture); + return loadFixture(timeBasedRewardsFixture); +} + +for (const isTimeBased of [false, true]) { + const description = getDescription(isTimeBased); + + describe(`${description}PoolLens: Rewards Summary`, () => { + beforeEach(async () => { + [account] = await ethers.getSigners(); + ({ + comptroller, + vBUSD, + vWBTC, + rewardDistributor1, + rewardToken1, + rewardDistributor2, + rewardToken2, + rewardDistributor3, + rewardToken3, + poolLens, + startBlock, + } = await setup(isTimeBased)); + }); + + it("Should get summary for all markets", async () => { + // Mine some blocks so deltaBlocks != 0 + await mineUpTo(startBlock + 1000); + + const accountAddress = await account.getAddress(); + + const pendingRewards = await poolLens.getPendingRewards(accountAddress, comptroller.address); + + expect(comptroller.getAllMarkets).to.have.been.calledOnce; + expect(comptroller.getRewardDistributors).to.have.been.calledOnce; + + expect(rewardDistributor1.rewardToken).to.have.been.calledOnce; + expect(rewardDistributor2.rewardToken).to.have.been.calledOnce; + expect(rewardDistributor3.rewardToken).to.have.been.calledOnce; + + expect(rewardDistributor1.rewardTokenAccrued).to.have.been.calledOnce; + expect(rewardDistributor2.rewardTokenAccrued).to.have.been.calledOnce; + expect(rewardDistributor3.rewardTokenAccrued).to.have.been.calledOnce; + + // Should be called once per market + if (isTimeBased) { + expect(rewardDistributor1.rewardTokenBorrowStateTimeBased).to.have.been.callCount(2); + expect(rewardDistributor2.rewardTokenBorrowStateTimeBased).to.have.been.callCount(2); + expect(rewardDistributor3.rewardTokenBorrowStateTimeBased).to.have.been.callCount(2); - // Should be called once per market - expect(rewardDistributor1.rewardTokenBorrowState).to.have.been.callCount(2); - expect(rewardDistributor2.rewardTokenBorrowState).to.have.been.callCount(2); - expect(rewardDistributor3.rewardTokenBorrowState).to.have.been.callCount(2); + expect(rewardDistributor1.rewardTokenSupplyStateTimeBased).to.have.been.callCount(2); + expect(rewardDistributor2.rewardTokenSupplyStateTimeBased).to.have.been.callCount(2); + expect(rewardDistributor3.rewardTokenSupplyStateTimeBased).to.have.been.callCount(2); + } else { + expect(rewardDistributor1.rewardTokenBorrowState).to.have.been.callCount(2); + expect(rewardDistributor2.rewardTokenBorrowState).to.have.been.callCount(2); + expect(rewardDistributor3.rewardTokenBorrowState).to.have.been.callCount(2); - expect(rewardDistributor1.rewardTokenSupplyState).to.have.been.callCount(2); - expect(rewardDistributor2.rewardTokenSupplyState).to.have.been.callCount(2); - expect(rewardDistributor3.rewardTokenSupplyState).to.have.been.callCount(2); + expect(rewardDistributor1.rewardTokenSupplyState).to.have.been.callCount(2); + expect(rewardDistributor2.rewardTokenSupplyState).to.have.been.callCount(2); + expect(rewardDistributor3.rewardTokenSupplyState).to.have.been.callCount(2); + } - // Should be called once per reward token configured - expect(vBUSD.borrowIndex).to.have.been.callCount(3); - expect(vWBTC.borrowIndex).to.have.been.callCount(3); + // Should be called once per reward token configured + expect(vBUSD.borrowIndex).to.have.been.callCount(3); + expect(vWBTC.borrowIndex).to.have.been.callCount(3); - // Should be called once per reward token configured - expect(vBUSD.totalBorrows).to.have.been.callCount(3); - expect(vWBTC.totalSupply).to.have.been.callCount(3); + // Should be called once per reward token configured + expect(vBUSD.totalBorrows).to.have.been.callCount(3); + expect(vWBTC.totalSupply).to.have.been.callCount(3); - const EXPECTED_OUTPUT = [ - [ - rewardDistributor1.address, - rewardToken1.address, - BigNumber.from(convertToUnit(50, 18)), + const EXPECTED_OUTPUT = [ [ - [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], - [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + rewardDistributor1.address, + rewardToken1.address, + BigNumber.from(convertToUnit(50, 18)), + [ + [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], + [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + ], ], - ], - [ - rewardDistributor2.address, - rewardToken2.address, - BigNumber.from(convertToUnit(50, 18)), [ - [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], - [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + rewardDistributor2.address, + rewardToken2.address, + BigNumber.from(convertToUnit(50, 18)), + [ + [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], + [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + ], ], - ], - [ - rewardDistributor3.address, - rewardToken3.address, - BigNumber.from(convertToUnit(50, 18)), [ - [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], - [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + rewardDistributor3.address, + rewardToken3.address, + BigNumber.from(convertToUnit(50, 18)), + [ + [vBUSD.address, BigNumber.from(convertToUnit(10, 18))], + [vWBTC.address, BigNumber.from(convertToUnit(0.0000001, 18))], + ], ], - ], - ]; - expect(pendingRewards).to.have.deep.members(EXPECTED_OUTPUT); - }); - - it("Should return accrued rewards if borrower borrowed before initialization", async () => { - comptroller.getRewardDistributors.returns([rewardDistributor3.address]); - rewardDistributor3.rewardTokenBorrowerIndex.returns(0); // Borrower borrowed before initialization, so they have no index - rewardDistributor3.rewardTokenBorrowState.returns({ - index: convertToUnit(1, 36), // Current index is 1.0, double scale - block: await ethers.provider.getBlockNumber(), - lastRewardingBlock: 0, + ]; + expect(pendingRewards).to.have.deep.members(EXPECTED_OUTPUT); }); - rewardDistributor3.INITIAL_INDEX.returns(convertToUnit(0.6, 36)); // Should start accruing rewards at 0.6 of the current index - - const pendingRewards = await poolLens.getPendingRewards(await account.getAddress(), comptroller.address); - - // The user has 0.000001% of the market share, and the reward accumulated since the beginning of the - // distribution is proportional to 1.0 (current index) - 0.6 (initial index) = 0.4. The index gets increased - // according to the configured distribution speed, so the speed is already included in the computation. - // The user should have accrued 0.000001% * 0.4 = 0.00000004 of the reward token (assuming the reward token - // has 18 decimals). - // - // Supplier reward is zero, because the global supply index is equal to the user's supply index, and there - // are no blocks between the last update and the current block. Thus, the results account only for the - // borrower reward. - - const EXPECTED_OUTPUT = [ - [ - rewardDistributor3.address, - rewardToken3.address, - BigNumber.from(convertToUnit(50, 18)), + + it("Should return accrued rewards if borrower borrowed before initialization", async () => { + comptroller.getRewardDistributors.returns([rewardDistributor3.address]); + rewardDistributor3.rewardTokenBorrowerIndex.returns(0); // Borrower borrowed before initialization, so they have no index + + let blockNumberOrTimestamp = (await ethers.provider.getBlock("latest")).number; + if (isTimeBased) { + blockNumberOrTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + rewardDistributor3.rewardTokenBorrowStateTimeBased.returns({ + index: convertToUnit(1, 36), // Current index is 1.0, double scale + timestamp: blockNumberOrTimestamp, + lastRewardingTimestamp: 0, + }); + } else { + rewardDistributor3.rewardTokenBorrowState.returns({ + index: convertToUnit(1, 36), // Current index is 1.0, double scale + block: blockNumberOrTimestamp, + lastRewardingBlock: 0, + }); + } + rewardDistributor3.INITIAL_INDEX.returns(convertToUnit(0.6, 36)); // Should start accruing rewards at 0.6 of the current index + + const pendingRewards = await poolLens.getPendingRewards(await account.getAddress(), comptroller.address); + + // The user has 0.000001% of the market share, and the reward accumulated since the beginning of the + // distribution is proportional to 1.0 (current index) - 0.6 (initial index) = 0.4. The index gets increased + // according to the configured distribution speed, so the speed is already included in the computation. + // The user should have accrued 0.000001% * 0.4 = 0.00000004 of the reward token (assuming the reward token + // has 18 decimals). + // + // Supplier reward is zero, because the global supply index is equal to the user's supply index, and there + // are no blocks between the last update and the current block. Thus, the results account only for the + // borrower reward. + + const EXPECTED_OUTPUT = [ [ - [vBUSD.address, BigNumber.from(convertToUnit("0.000000004", 18))], - [vWBTC.address, BigNumber.from(convertToUnit("0.000000004", 18))], + rewardDistributor3.address, + rewardToken3.address, + BigNumber.from(convertToUnit(50, 18)), + [ + [vBUSD.address, BigNumber.from(convertToUnit("0.000000004", 18))], + [vWBTC.address, BigNumber.from(convertToUnit("0.000000004", 18))], + ], ], - ], - ]; - expect(pendingRewards).to.have.deep.members(EXPECTED_OUTPUT); + ]; + expect(pendingRewards).to.have.deep.members(EXPECTED_OUTPUT); + }); + + it("Should revert when mode of PoolLens and RewardsDistributor differ", async () => { + await mineUpTo(startBlock + 1000); + + const accountAddress = await account.getAddress(); + + if (isTimeBased) { + rewardDistributor3.isTimeBased.returns(false); + } else { + rewardDistributor3.isTimeBased.returns(true); + } + await expect(poolLens.getPendingRewards(accountAddress, comptroller.address)).to.be.revertedWith( + "Inconsistent Reward mode", + ); + }); }); -}); +} diff --git a/tests/hardhat/Rewards.ts b/tests/hardhat/Rewards.ts index bf4e9b218..0a36265c7 100644 --- a/tests/hardhat/Rewards.ts +++ b/tests/hardhat/Rewards.ts @@ -5,6 +5,7 @@ import { expect } from "chai"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; +import { BSC_BLOCKS_PER_YEAR } from "../../helpers/deploymentConfig"; import { convertToUnit } from "../../helpers/utils"; import { AccessControlManager, @@ -19,6 +20,7 @@ import { VToken, } from "../../typechain"; import { deployVTokenBeacon, makeVToken } from "./util/TokenTestHelpers"; +import { getDescription } from "./util/descriptionHelpers"; // Disable a warning about mixing beacons and transparent proxies upgrades.silenceWarnings(); @@ -35,8 +37,9 @@ let xvs: MockToken; let fakePriceOracle: FakeContract; let fakeAccessControlManager: FakeContract; const maxLoopsLimit = 150; +let blocksPerYear = BSC_BLOCKS_PER_YEAR; // for block based contracts -async function rewardsFixture() { +async function rewardsFixture(isTimeBased: boolean) { [root] = await ethers.getSigners(); fakeAccessControlManager = await smock.fake("AccessControlManager"); @@ -60,6 +63,8 @@ async function rewardsFixture() { const btcPrice = "21000.34"; const daiPrice = "1"; + const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); + fakePriceOracle.getUnderlyingPrice.returns((args: any) => { if (vDAI && vWBTC) { if (args[0] === vDAI.address) { @@ -95,14 +100,31 @@ async function rewardsFixture() { _minLiquidatableCollateral, ); + if (isTimeBased) { + blocksPerYear = 0; + } + // Deploy VTokens - const vTokenBeacon = await deployVTokenBeacon(); + const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); + const vTokenBeacon = await deployVTokenBeacon(undefined, maxBorrowRateMantissa, isTimeBased, blocksPerYear); vWBTC = await makeVToken({ underlying: mockWBTC, comptroller: comptrollerProxy, accessControlManager: fakeAccessControlManager, admin: root, beacon: vTokenBeacon, + isTimeBased: isTimeBased, + blocksPerYear: blocksPerYear, + }); + + vDAI = await makeVToken({ + underlying: mockDAI, + comptroller: comptrollerProxy, + accessControlManager: fakeAccessControlManager, + admin: root, + beacon: vTokenBeacon, + isTimeBased: isTimeBased, + blocksPerYear: blocksPerYear, }); const wbtcInitialSupply = parseUnits("10", 8); @@ -149,21 +171,17 @@ async function rewardsFixture() { xvs = await MockToken.deploy("Venus Token", "XVS", 18); // Configure rewards for pool - const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); - rewardsDistributor = (await upgrades.deployProxy(RewardsDistributor, [ - comptrollerProxy.address, - xvs.address, - maxLoopsLimit, - fakeAccessControlManager.address, - ])) as RewardsDistributor; - - const rewardsDistributor2 = await upgrades.deployProxy(RewardsDistributor, [ - comptrollerProxy.address, - mockDAI.address, - maxLoopsLimit, - fakeAccessControlManager.address, - ]); - + rewardsDistributor = (await upgrades.deployProxy( + RewardsDistributor, + [comptrollerProxy.address, xvs.address, maxLoopsLimit, fakeAccessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear] }, + )) as RewardsDistributor; + + const rewardsDistributor2 = await upgrades.deployProxy( + RewardsDistributor, + [comptrollerProxy.address, mockDAI.address, maxLoopsLimit, fakeAccessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear] }, + ); const initialXvs = convertToUnit(1000000, 18); await xvs.faucet(initialXvs); await xvs.transfer(rewardsDistributor.address, initialXvs); @@ -186,226 +204,283 @@ async function rewardsFixture() { ); } -describe("Rewards: Tests", async function () { - /** - * Deploying required contracts along with the poolRegistry. - */ - beforeEach(async function () { - await rewardsFixture(); - fakeAccessControlManager.isAllowedToCall.reset(); - fakeAccessControlManager.isAllowedToCall.returns(true); - }); - - it("Reverts if setting the speed is prohibited by ACM", async () => { - fakeAccessControlManager.isAllowedToCall - .whenCalledWith(root.address, "setRewardTokenSpeeds(address[],uint256[],uint256[])") - .returns(false); - await expect( - rewardsDistributor.setRewardTokenSpeeds( - [vWBTC.address, vDAI.address], - [convertToUnit(0.5, 18), convertToUnit(0.5, 18)], - [convertToUnit(0.5, 18), convertToUnit(0.5, 18)], - ), - ).to.be.revertedWithCustomError(rewardsDistributor, "Unauthorized"); - }); - - it("Should have correct btc balance", async function () { - const [owner] = await ethers.getSigners(); - - const btcBalance = await mockWBTC.balanceOf(owner.address); - - expect(btcBalance).equal(convertToUnit(1000, 8)); - }); - - it("Pool should have correct name", async function () { - // Get all pools list. - const pools = await poolRegistry.callStatic.getAllPools(); - expect(pools[0].name).equal("Pool 1"); - }); - - it("Rewards distributor should have correct balance", async function () { - expect(await xvs.balanceOf(rewardsDistributor.address)).equal(convertToUnit(1000000, 18)); - }); - - it("Should have correct market addresses", async function () { - const [owner] = await ethers.getSigners(); - - const res = await comptrollerProxy.getAssetsIn(owner.address); - expect(res[0]).equal(vDAI.address); - expect(res[1]).equal(vWBTC.address); - }); - - it("Comptroller returns correct reward speeds", async function () { - const res = await comptrollerProxy.getRewardsByMarket(vDAI.address); - expect(res[0][0]).equal(xvs.address); - expect(res[0][1].toString()).equal(convertToUnit(0.5, 18)); - expect(res[0][2].toString()).equal(convertToUnit(0.5, 18)); - expect(res[1][0]).equal(mockDAI.address); - expect(res[1][1].toString()).equal(convertToUnit(0.3, 18)); - expect(res[1][2].toString()).equal(convertToUnit(0.1, 18)); - }); - - it("Can add reward distributors with duplicate reward tokens", async function () { - const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); - rewardsDistributor = (await upgrades.deployProxy(RewardsDistributor, [ - comptrollerProxy.address, - xvs.address, - maxLoopsLimit, - fakeAccessControlManager.address, - ])) as RewardsDistributor; - - await expect(comptrollerProxy.addRewardsDistributor(rewardsDistributor.address)) - .to.emit(comptrollerProxy, "NewRewardsDistributor") - .withArgs(rewardsDistributor.address, xvs.address); - }); - - it("Emits event correctly", async () => { - const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); - rewardsDistributor = (await upgrades.deployProxy(RewardsDistributor, [ - comptrollerProxy.address, - mockWBTC.address, - maxLoopsLimit, - fakeAccessControlManager.address, - ])) as RewardsDistributor; - - await expect(comptrollerProxy.addRewardsDistributor(rewardsDistributor.address)) - .to.emit(comptrollerProxy, "NewRewardsDistributor") - .withArgs(rewardsDistributor.address, mockWBTC.address); - }); - - it("Claim XVS", async () => { - const [, user1, user2] = await ethers.getSigners(); - - await mockWBTC.connect(user1).faucet(convertToUnit(100, 8)); - await mockDAI.connect(user2).faucet(convertToUnit(10000, 18)); - - await mockWBTC.connect(user1).approve(vWBTC.address, convertToUnit(10, 8)); - await vWBTC.connect(user1).mint(convertToUnit(10, 8)); - - await rewardsDistributor.functions["claimRewardToken(address,address[])"](user1.address, [ - vWBTC.address, - vDAI.address, - ]); - - /* - Formula: (supplyIndex * supplyTokens * blocksDelta) + (borrowIndex * borrowTokens * blocksDelta) - 0.5 * 10 * 5 = 25 - */ - expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal(convertToUnit(0.25, 18)); - - await mockDAI.connect(user2).approve(vDAI.address, convertToUnit(10000, 18)); - await vDAI.connect(user2).mint(convertToUnit(10000, 18)); - await vWBTC.connect(user2).borrow(convertToUnit(0.01, 8)); - - await rewardsDistributor["claimRewardToken(address,address[])"](user2.address, [vWBTC.address, vDAI.address]); - - expect((await xvs.balanceOf(user2.address)).toString()).to.be.equal(convertToUnit("1.40909090909090909", 18)); - }); - - it("Contributor Rewards", async () => { - const [, user1] = await ethers.getSigners(); - - expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal("0"); - - await rewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); - - await mine(1000); - await rewardsDistributor.updateContributorRewards(user1.address); - - await rewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); - - /* - Formula: speed * blocks - 0.5 * 1001 = 500.5 - */ - expect((await xvs.balanceOf(user1.address)).toString()).be.equal(convertToUnit(500.5, 18)); - }); - - it("Multiple reward distributors with same reward token", async () => { - const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); - const newRewardsDistributor = (await upgrades.deployProxy(RewardsDistributor, [ - comptrollerProxy.address, - xvs.address, - maxLoopsLimit, - fakeAccessControlManager.address, - ])) as RewardsDistributor; - - await comptrollerProxy.addRewardsDistributor(newRewardsDistributor.address); - - const initialXvs = convertToUnit(1000000, 18); - await xvs.faucet(initialXvs); - await xvs.transfer(newRewardsDistributor.address, initialXvs); - - const [, user1] = await ethers.getSigners(); - - expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal("0"); - - await rewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); - await newRewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); - - await mine(1000); - await rewardsDistributor.updateContributorRewards(user1.address); - await newRewardsDistributor.updateContributorRewards(user1.address); - - await rewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); - await newRewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); - - /* - Reward Distributor 1 - Formula: speed * blocks - 0.5 * 1001 = 500.5 - - Reward Distributor 2 - Formula: speed * blocks - 0.5 * 1003 = 501.5 - - Total xvs reward = 500.5 + 501.5 = 1002 - */ - expect((await xvs.balanceOf(user1.address)).toString()).be.equal(convertToUnit(1002, 18)); - }); - - it("pause rewards", async () => { - const [, user1, user2] = await ethers.getSigners(); +for (const isTimeBased of [false, true]) { + const description: string = getDescription(isTimeBased); + + describe(`${description}Rewards: Tests`, async function () { + /** + * Deploying required contracts along with the poolRegistry. + */ + beforeEach(async function () { + await rewardsFixture(isTimeBased); + fakeAccessControlManager.isAllowedToCall.reset(); + fakeAccessControlManager.isAllowedToCall.returns(true); + }); + + it("should revert when setting LastRewarding block or timestamp on invalid operation", async () => { + let lastRewardingBlockOrTimestamp; + if (!isTimeBased) { + lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).timestamp + 2; + + await expect( + rewardsDistributor.setLastRewardingBlockTimestamps( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + ), + ).to.be.revertedWith("Time-based operation only"); + } else { + lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).number + 2; + + await expect( + rewardsDistributor.setLastRewardingBlocks( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + ), + ).to.be.revertedWith("Block-based operation only"); + } + }); + + it("Reverts if setting the speed is prohibited by ACM", async () => { + fakeAccessControlManager.isAllowedToCall + .whenCalledWith(root.address, "setRewardTokenSpeeds(address[],uint256[],uint256[])") + .returns(false); + await expect( + rewardsDistributor.setRewardTokenSpeeds( + [vWBTC.address, vDAI.address], + [convertToUnit(0.5, 18), convertToUnit(0.5, 18)], + [convertToUnit(0.5, 18), convertToUnit(0.5, 18)], + ), + ).to.be.revertedWithCustomError(rewardsDistributor, "Unauthorized"); + }); + + it("Should have correct btc balance", async function () { + const [owner] = await ethers.getSigners(); + + const btcBalance = await mockWBTC.balanceOf(owner.address); + + expect(btcBalance).equal(convertToUnit(1000, 8)); + }); + + it("Pool should have correct name", async function () { + // Get all pools list. + const pools = await poolRegistry.callStatic.getAllPools(); + expect(pools[0].name).equal("Pool 1"); + }); + + it("Rewards distributor should have correct balance", async function () { + expect(await xvs.balanceOf(rewardsDistributor.address)).equal(convertToUnit(1000000, 18)); + }); + + it("Should have correct market addresses", async function () { + const [owner] = await ethers.getSigners(); + + const res = await comptrollerProxy.getAssetsIn(owner.address); + expect(res[0]).equal(vDAI.address); + expect(res[1]).equal(vWBTC.address); + }); + + it("Comptroller returns correct reward speeds", async function () { + const res = await comptrollerProxy.getRewardsByMarket(vDAI.address); + expect(res[0][0]).equal(xvs.address); + expect(res[0][1].toString()).equal(convertToUnit(0.5, 18)); + expect(res[0][2].toString()).equal(convertToUnit(0.5, 18)); + expect(res[1][0]).equal(mockDAI.address); + expect(res[1][1].toString()).equal(convertToUnit(0.3, 18)); + expect(res[1][2].toString()).equal(convertToUnit(0.1, 18)); + }); + + it("Can add reward distributors with duplicate reward tokens", async function () { + const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); + rewardsDistributor = (await upgrades.deployProxy( + RewardsDistributor, + [comptrollerProxy.address, xvs.address, maxLoopsLimit, fakeAccessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear] }, + )) as RewardsDistributor; + + await expect(comptrollerProxy.addRewardsDistributor(rewardsDistributor.address)) + .to.emit(comptrollerProxy, "NewRewardsDistributor") + .withArgs(rewardsDistributor.address, xvs.address); + }); + + it("Emits event correctly", async () => { + const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); + rewardsDistributor = (await upgrades.deployProxy( + RewardsDistributor, + [comptrollerProxy.address, mockWBTC.address, maxLoopsLimit, fakeAccessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear] }, + )) as RewardsDistributor; + + await expect(comptrollerProxy.addRewardsDistributor(rewardsDistributor.address)) + .to.emit(comptrollerProxy, "NewRewardsDistributor") + .withArgs(rewardsDistributor.address, mockWBTC.address); + }); + + it("Claim XVS", async () => { + const [, user1, user2] = await ethers.getSigners(); + + await mockWBTC.connect(user1).faucet(convertToUnit(100, 8)); + await mockDAI.connect(user2).faucet(convertToUnit(10000, 18)); + + await mockWBTC.connect(user1).approve(vWBTC.address, convertToUnit(10, 8)); + await vWBTC.connect(user1).mint(convertToUnit(10, 8)); + + await rewardsDistributor.functions["claimRewardToken(address,address[])"](user1.address, [ + vWBTC.address, + vDAI.address, + ]); + + /* + Formula: (supplyIndex * supplyTokens * blocksDelta) + (borrowIndex * borrowTokens * blocksDelta) + 0.5 * 10 * 5 = 25 + */ + expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal(convertToUnit(0.25, 18)); + + await mockDAI.connect(user2).approve(vDAI.address, convertToUnit(10000, 18)); + await vDAI.connect(user2).mint(convertToUnit(10000, 18)); + await vWBTC.connect(user2).borrow(convertToUnit(0.01, 8)); + + await rewardsDistributor["claimRewardToken(address,address[])"](user2.address, [vWBTC.address, vDAI.address]); + + expect((await xvs.balanceOf(user2.address)).toString()).to.be.equal(convertToUnit("1.40909090909090909", 18)); + }); + + it("Contributor Rewards", async () => { + const [, user1] = await ethers.getSigners(); + + expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal("0"); + + await rewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); + + await mine(1000); + await rewardsDistributor.updateContributorRewards(user1.address); + + await rewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); + + /* + Formula: speed * blocks + 0.5 * 1001 = 500.5 + */ + expect((await xvs.balanceOf(user1.address)).toString()).be.equal(convertToUnit(500.5, 18)); + }); + + it("Multiple reward distributors with same reward token", async () => { + const RewardsDistributor = await ethers.getContractFactory("RewardsDistributor"); + const newRewardsDistributor = (await upgrades.deployProxy( + RewardsDistributor, + [comptrollerProxy.address, xvs.address, maxLoopsLimit, fakeAccessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear] }, + )) as RewardsDistributor; + + await comptrollerProxy.addRewardsDistributor(newRewardsDistributor.address); + + const initialXvs = convertToUnit(1000000, 18); + await xvs.faucet(initialXvs); + await xvs.transfer(newRewardsDistributor.address, initialXvs); + + const [, user1] = await ethers.getSigners(); + + expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal("0"); + + await rewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); + await newRewardsDistributor.setContributorRewardTokenSpeed(user1.address, convertToUnit(0.5, 18)); + + await mine(1000); + await rewardsDistributor.updateContributorRewards(user1.address); + await newRewardsDistributor.updateContributorRewards(user1.address); + + await rewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); + await newRewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]); + + /* + Reward Distributor 1 + Formula: speed * blocks + 0.5 * 1001 = 500.5 + + Reward Distributor 2 + Formula: speed * blocks + 0.5 * 1003 = 501.5 + + Total xvs reward = 500.5 + 501.5 = 1002 + */ + expect((await xvs.balanceOf(user1.address)).toString()).be.equal(convertToUnit(1002, 18)); + }); - await mockWBTC.connect(user1).faucet(convertToUnit(100, 8)); - await mockDAI.connect(user2).faucet(convertToUnit(10000, 18)); + it("pause rewards", async () => { + const [, user1, user2] = await ethers.getSigners(); - await mockWBTC.connect(user1).approve(vWBTC.address, convertToUnit(10, 8)); - await vWBTC.connect(user1).mint(convertToUnit(10, 8)); + await mockWBTC.connect(user1).faucet(convertToUnit(100, 8)); + await mockDAI.connect(user2).faucet(convertToUnit(10000, 18)); - let lastRewardingBlock = (await ethers.provider.getBlockNumber()) + 2; + await mockWBTC.connect(user1).approve(vWBTC.address, convertToUnit(10, 8)); + await vWBTC.connect(user1).mint(convertToUnit(10, 8)); - await rewardsDistributor.setLastRewardingBlocks( - [vWBTC.address, vDAI.address], - [lastRewardingBlock, lastRewardingBlock], - [lastRewardingBlock, lastRewardingBlock], - ); + let lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).number + 2; + if (isTimeBased) { + lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).timestamp + 2; + } - await mine(100); + if (!isTimeBased) { + await rewardsDistributor.setLastRewardingBlocks( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + ); + } else { + await rewardsDistributor.setLastRewardingBlockTimestamps( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + ); + } - await rewardsDistributor.functions["claimRewardToken(address,address[])"](user1.address, [ - vWBTC.address, - vDAI.address, - ]); + await mine(100); - expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal(convertToUnit(0.5, 18)); + await rewardsDistributor.functions["claimRewardToken(address,address[])"](user1.address, [ + vWBTC.address, + vDAI.address, + ]); - await expect( - rewardsDistributor.setLastRewardingBlocks( - [vWBTC.address, vDAI.address], - [lastRewardingBlock - 10, lastRewardingBlock - 10], - [lastRewardingBlock - 10, lastRewardingBlock - 10], - ), - ).to.be.revertedWith("setting last rewarding block in the past is not allowed"); + expect((await xvs.balanceOf(user1.address)).toString()).to.be.equal(convertToUnit(0.5, 18)); - lastRewardingBlock = (await ethers.provider.getBlockNumber()) + 2; + if (!isTimeBased) { + await expect( + rewardsDistributor.setLastRewardingBlocks( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + ), + ).to.be.revertedWith("setting last rewarding block in the past is not allowed"); + } else { + await expect( + rewardsDistributor.setLastRewardingBlockTimestamps( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + [lastRewardingBlockOrTimestamp - 10, lastRewardingBlockOrTimestamp - 10], + ), + ).to.be.revertedWith("setting last rewarding timestamp in the past is not allowed"); + } - await expect( - rewardsDistributor.setLastRewardingBlocks( - [vWBTC.address, vDAI.address], - [lastRewardingBlock, lastRewardingBlock], - [lastRewardingBlock, lastRewardingBlock], - ), - ).to.be.revertedWith("this RewardsDistributor is already locked"); + if (!isTimeBased) { + lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).number + 2; + await expect( + rewardsDistributor.setLastRewardingBlocks( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + ), + ).to.be.revertedWith("this RewardsDistributor is already locked"); + } else { + lastRewardingBlockOrTimestamp = (await ethers.provider.getBlock("latest")).timestamp + 2; + await expect( + rewardsDistributor.setLastRewardingBlockTimestamps( + [vWBTC.address, vDAI.address], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + [lastRewardingBlockOrTimestamp, lastRewardingBlockOrTimestamp], + ), + ).to.be.revertedWith("this RewardsDistributor is already locked"); + } + }); }); -}); +} diff --git a/tests/hardhat/Shortfall.ts b/tests/hardhat/Shortfall.ts index 01033328b..c9b48c3c8 100644 --- a/tests/hardhat/Shortfall.ts +++ b/tests/hardhat/Shortfall.ts @@ -7,6 +7,7 @@ import { constants } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; +import { BSC_BLOCKS_PER_YEAR } from "../../helpers/deploymentConfig"; import { AddressOne, convertToUnit } from "../../helpers/utils"; import { AccessControlManager, @@ -22,6 +23,7 @@ import { VToken, VToken__factory, } from "../../typechain"; +import { getDescription } from "./util/descriptionHelpers"; const { expect } = chai; chai.use(smock.matchers); @@ -44,8 +46,12 @@ let vFloki: MockContract; let comptroller: FakeContract; let fakePriceOracle: FakeContract; -let riskFundBalance = "10000"; +const riskFundBalance = "10000"; const minimumPoolBadDebt = "10000"; +let isTimeBased = false; // for block based contracts +let blocksPerYear = BSC_BLOCKS_PER_YEAR; // for block based contracts +let waitForFirstBidder = 100; // for block based contracts +let nextBidderBlockOrTimestampLimit = 100; // for block based contracts let poolAddress; /** @@ -56,16 +62,18 @@ async function shortfallFixture() { mockBUSD = await MockBUSD.deploy("BUSD", "BUSD", 18); await mockBUSD.faucet(convertToUnit(100000, 18)); + const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); + const AccessControlManagerFactor = await ethers.getContractFactory("AccessControlManager"); accessControlManager = await AccessControlManagerFactor.deploy(); fakeRiskFund = await smock.fake("IRiskFund"); const Shortfall = await smock.mock("Shortfall"); - shortfall = await upgrades.deployProxy(Shortfall, [ - fakeRiskFund.address, - parseUnits(minimumPoolBadDebt, "18"), - accessControlManager.address, - ]); + shortfall = await upgrades.deployProxy( + Shortfall, + [fakeRiskFund.address, parseUnits(minimumPoolBadDebt, "18"), accessControlManager.address], + { constructorArgs: [isTimeBased, blocksPerYear, nextBidderBlockOrTimestampLimit, waitForFirstBidder] }, + ); [owner, someone, bidder1, bidder2] = await ethers.getSigners(); @@ -98,9 +106,9 @@ async function shortfallFixture() { }); const VToken = await smock.mock("VToken"); - vDAI = await VToken.deploy(); - vWBTC = await VToken.deploy(); - vFloki = await VToken.deploy(); + vDAI = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); + vWBTC = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); + vFloki = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); await vWBTC.setVariable("decimals", 8); vDAI.decimals.returns(18); @@ -177,634 +185,816 @@ async function shortfallFixture() { await mockFloki.connect(bidder2).approve(shortfall.address, parseUnits("150", 18)); } -async function setup() { - await loadFixture(shortfallFixture); -} +async function timeBasedhortfallFixture() { + isTimeBased = true; + blocksPerYear = 0; + waitForFirstBidder = 300; + nextBidderBlockOrTimestampLimit = 300; -describe("Shortfall: Tests", async function () { - describe("setters", async function () { - beforeEach(setup); + const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); - describe("updatePoolRegistry", async function () { - it("reverts on invalid PoolRegistry address", async function () { - await expect(shortfall.updatePoolRegistry(constants.AddressZero)).to.be.revertedWithCustomError( - shortfall, - "ZeroAddressNotAllowed", - ); - }); + const MockBUSD = await ethers.getContractFactory("MockToken"); + mockBUSD = await MockBUSD.deploy("BUSD", "BUSD", 18); + await mockBUSD.faucet(convertToUnit(100000, 18)); - it("fails if called by a non-owner", async function () { - await expect(shortfall.connect(someone).updatePoolRegistry(poolRegistry.address)).to.be.rejectedWith( - "Ownable: caller is not the owner", - ); - }); + const AccessControlManagerFactor = await ethers.getContractFactory("AccessControlManager"); + accessControlManager = await AccessControlManagerFactor.deploy(); + fakeRiskFund = await smock.fake("IRiskFund"); - it("emits PoolRegistryUpdated event", async function () { - const tx = shortfall.updatePoolRegistry(someone.address); - await expect(tx).to.emit(shortfall, "PoolRegistryUpdated").withArgs(poolRegistry.address, someone.address); - }); - }); + const Shortfall = await smock.mock("Shortfall"); + shortfall = await upgrades.deployProxy( + Shortfall, + [fakeRiskFund.address, parseUnits(minimumPoolBadDebt, "18"), accessControlManager.address], + { + constructorArgs: [isTimeBased, blocksPerYear, nextBidderBlockOrTimestampLimit, waitForFirstBidder], + }, + ); + [owner, someone, bidder1, bidder2] = await ethers.getSigners(); - describe("updateMinimumPoolBadDebt", async function () { - it("fails if called by a non permissioned account", async function () { - await expect(shortfall.connect(someone).updateMinimumPoolBadDebt(1)).to.be.reverted; - }); + poolRegistry = await smock.fake("PoolRegistry"); - it("updates minimumPoolBadDebt in storage", async function () { - await shortfall.updateMinimumPoolBadDebt(1); - expect(await shortfall.minimumPoolBadDebt()).to.equal(1); - }); + await shortfall.updatePoolRegistry(poolRegistry.address); - it("emits MinimumPoolBadDebtUpdated event", async function () { - const tx = shortfall.updateMinimumPoolBadDebt(1); - await expect(tx) - .to.emit(shortfall, "MinimumPoolBadDebtUpdated") - .withArgs(parseUnits(minimumPoolBadDebt, "18"), 1); - }); - }); + // Deploy Mock Tokens + const MockDAI = await ethers.getContractFactory("MockToken"); + mockDAI = await MockDAI.deploy("MakerDAO", "DAI", 18); + await mockDAI.faucet(convertToUnit(1000000000, 18)); - describe("waitForFirstBidder", async function () { - it("fails if called by a non permissioned account", async function () { - await expect(shortfall.connect(someone).updateWaitForFirstBidder(200)).to.be.reverted; - }); + const MockWBTC = await ethers.getContractFactory("MockToken"); + mockWBTC = await MockWBTC.deploy("Bitcoin", "BTC", 8); + await mockWBTC.faucet(convertToUnit(1000000000, 8)); - it("updates updateWaitForFirstBidder in storage", async function () { - await shortfall.updateWaitForFirstBidder(200); - expect(await shortfall.waitForFirstBidder()).to.equal(200); - }); + const MockFloki = await ethers.getContractFactory("MockDeflatingToken"); + mockFloki = await MockFloki.deploy(convertToUnit(1000000000, 18)); - it("emits WaitForFirstBidderUpdated event", async function () { - const tx = shortfall.updateWaitForFirstBidder(200); - await expect(tx).to.emit(shortfall, "WaitForFirstBidderUpdated").withArgs(100, 200); - }); - }); + const Comptroller = await smock.mock("Comptroller"); + comptroller = await Comptroller.deploy(poolRegistry.address); + poolAddress = comptroller.address; - describe("updateNextBidderBlockLimit", async function () { - it("fails if called by a non permissioned account", async function () { - await accessControlManager.revokeCallPermission( - shortfall.address, - "updateNextBidderBlockLimit(uint256)", - owner.address, - ); - await expect(shortfall.connect(someone).updateNextBidderBlockLimit(1)).to.be.reverted; - await accessControlManager.giveCallPermission( - shortfall.address, - "updateNextBidderBlockLimit(uint256)", - owner.address, - ); - }); + poolRegistry.getPoolByComptroller.returns({ + name: "test", + creator: AddressOne, + comptroller: comptroller.address, + blockPosted: 0, + timestampPosted: 0, + }); - it("updates nextBidderBlockLimit in storage", async function () { - await shortfall.updateNextBidderBlockLimit(100); - expect(await shortfall.nextBidderBlockLimit()).to.equal(100); - }); + const VToken = await smock.mock("VToken"); + vDAI = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); + vWBTC = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); + vFloki = await VToken.deploy(isTimeBased, blocksPerYear, maxBorrowRateMantissa); - it("emits NextBidderBlockLimitUpdated event", async function () { - const tx = shortfall.updateNextBidderBlockLimit(110); - await expect(tx).to.emit(shortfall, "NextBidderBlockLimitUpdated").withArgs(100, 110); - }); - }); - }); + await vWBTC.setVariable("decimals", 8); + vDAI.decimals.returns(18); - describe("updateIncentiveBps", async function () { - it("fails if caller is not allowed", async function () { - await expect(shortfall.connect(someone).updateIncentiveBps(1)).to.be.reverted; - }); + vDAI.underlying.returns(mockDAI.address); + fakeRiskFund.convertibleBaseAsset.returns(mockDAI.address); + await vWBTC.setVariable("underlying", mockWBTC.address); + await vFloki.setVariable("underlying", mockFloki.address); - it("fails if new incentive BPS is set to 0", async function () { - await expect(shortfall.updateIncentiveBps(0)).to.be.revertedWith("incentiveBps must not be 0"); - }); + await vDAI.setVariable("shortfall", shortfall.address); + await vWBTC.setVariable("shortfall", shortfall.address); + await vFloki.setVariable("shortfall", shortfall.address); - it("emits IncentiveBpsUpdated event", async function () { - const tx = shortfall.updateIncentiveBps(2000); - await expect(tx).to.emit(shortfall, "IncentiveBpsUpdated").withArgs(1000, 2000); - }); + comptroller.getAllMarkets.returns(() => { + return [vDAI.address, vWBTC.address, vFloki.address]; }); - describe("placeBid", async function () { - beforeEach(setup); - let auctionStartBlock; + fakePriceOracle = await smock.fake("ResilientOracleInterface"); - async function startAuction() { - vDAI.badDebt.returns(parseUnits("10000", 18)); - await vDAI.setVariable("badDebt", parseUnits("10000", 18)); - vWBTC.badDebt.returns(parseUnits("2", 8)); - await vWBTC.setVariable("badDebt", parseUnits("2", 8)); - await shortfall.startAuction(poolAddress); - const auction = await shortfall.auctions(poolAddress); - auctionStartBlock = auction.startBlock; - } + const btcPrice = "21000.34"; + const daiPrice = "1"; - it("fails if auction is not active", async function () { - await expect(shortfall.placeBid(poolAddress, "10000", 0)).to.be.revertedWith("no on-going auction"); - }); + fakePriceOracle.getUnderlyingPrice.returns((args: any) => { + if (vDAI && vWBTC && vFloki) { + if (args[0] === vDAI.address) { + return convertToUnit(daiPrice, 18); + } else if (args[0] === vWBTC.address) { + return convertToUnit(btcPrice, 28); + } else { + return convertToUnit(1, 18); + } + } - it("fails if auction is stale", async function () { - await startAuction(); - await mine(100); - await expect(shortfall.placeBid(poolAddress, "10000", auctionStartBlock)).to.be.revertedWith( - "auction is stale, restart it", - ); - }); + return convertToUnit(1, 18); + }); - it("fails if bidBps is zero", async () => { - await startAuction(); - await expect(shortfall.placeBid(poolAddress, "0", auctionStartBlock)).to.be.revertedWith( - "basis points cannot be zero", - ); - }); + fakePriceOracle.getPrice.whenCalledWith(mockDAI.address).returns(convertToUnit(1, 18)); - it("fails if auctionStartBlock does not match the auction startBlock", async () => { - await startAuction(); - await mine(10); - const latestBlock = await ethers.provider.getBlock("latest"); - await expect(shortfall.placeBid(poolAddress, "0", latestBlock.number)).to.be.revertedWith( - "auction has been restarted", - ); - }); - }); + comptroller.oracle.returns(fakePriceOracle.address); - describe("LARGE_POOL_DEBT Scenario", async function () { - before(setup); - let startBlockNumber; + fakeRiskFund.getPoolsBaseAssetReserves.returns(parseUnits(riskFundBalance, 18)); - it("Should have debt and reserve", async function () { - vDAI.badDebt.returns(parseUnits("1000", 18)); - vWBTC.badDebt.returns(parseUnits("1", 8)); + // Access Control + await accessControlManager.giveCallPermission(shortfall.address, "updateIncentiveBps(uint256)", owner.address); - expect(await fakeRiskFund.getPoolsBaseAssetReserves(comptroller.address)).equal( - parseUnits(riskFundBalance, 18).toString(), - ); + await accessControlManager.giveCallPermission(shortfall.address, "updateMinimumPoolBadDebt(uint256)", owner.address); - expect(await vDAI.badDebt()).equal(parseUnits("1000", 18)); - expect(await vWBTC.badDebt()).equal(parseUnits("1", 8)); - }); + await accessControlManager.giveCallPermission(shortfall.address, "updateWaitForFirstBidder(uint256)", owner.address); - it("Should not be able to start auction when bad debt is low", async function () { - vDAI.badDebt.returns(parseUnits("20", 18)); - vWBTC.badDebt.returns(parseUnits("0.01", 8)); + await accessControlManager.giveCallPermission(shortfall.address, "pauseAuctions()", owner.address); - await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("pool bad debt is too low"); - }); + await accessControlManager.giveCallPermission(shortfall.address, "resumeAuctions()", owner.address); - it("can't restart when there is no ongoing auction", async function () { - await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith("no on-going auction"); - }); + await accessControlManager.giveCallPermission( + shortfall.address, + "updateNextBidderBlockLimit(uint256)", + owner.address, + ); - it("Should not be able to close auction when there is no active auction", async function () { - await expect(shortfall.closeAuction(poolAddress)).to.be.revertedWith("no on-going auction"); - }); + // setup bidders + // bidder 1 + await mockDAI.transfer(bidder1.address, parseUnits("500000", 18)); + await mockDAI.connect(bidder1).approve(shortfall.address, parseUnits("100000", 18)); + await mockWBTC.transfer(bidder1.address, parseUnits("50", 8)); + await mockWBTC.connect(bidder1).approve(shortfall.address, parseUnits("50", 8)); + await mockFloki.connect(owner).transfer(bidder1.address, parseUnits("200", 18)); + await mockFloki.connect(bidder1).approve(shortfall.address, parseUnits("150", 18)); + // // bidder 2 + await mockDAI.transfer(bidder2.address, parseUnits("500000", 18)); + await mockDAI.connect(bidder2).approve(shortfall.address, parseUnits("100000", 18)); + await mockWBTC.transfer(bidder2.address, parseUnits("50", 8)); + await mockWBTC.connect(bidder2).approve(shortfall.address, parseUnits("50", 8)); + await mockFloki.connect(owner).transfer(bidder2.address, parseUnits("200", 18)); + await mockFloki.connect(bidder2).approve(shortfall.address, parseUnits("150", 18)); +} - it("Should not be able to placeBid when there is no active auction", async function () { - vDAI.badDebt.returns(parseUnits("20", 18)); - vWBTC.badDebt.returns(parseUnits("0.01", 8)); - const auction = await shortfall.auctions(poolAddress); +async function setup(isTimeBased: boolean) { + if (!isTimeBased) { + await loadFixture(shortfallFixture); + return; + } + await loadFixture(timeBasedhortfallFixture); +} - await expect(shortfall.placeBid(poolAddress, "1800", auction.startBlock)).to.be.revertedWith( - "no on-going auction", - ); - }); +for (const isTimeBased of [false, true]) { + const description: string = getDescription(isTimeBased); - it("Start auction", async function () { - vDAI.badDebt.returns(parseUnits("10000", 18)); - await vDAI.setVariable("badDebt", parseUnits("10000", 18)); - vWBTC.badDebt.returns(parseUnits("2", 8)); - await vWBTC.setVariable("badDebt", parseUnits("2", 8)); + describe(`${description}Shortfall: Tests`, async function () { + describe("setters", async function () { + beforeEach(async function () { + await setup(isTimeBased); + }); - const receipt = await shortfall.startAuction(poolAddress); - startBlockNumber = receipt.blockNumber; + describe("updatePoolRegistry", async function () { + it("reverts on invalid PoolRegistry address", async function () { + await expect(shortfall.updatePoolRegistry(constants.AddressZero)).to.be.revertedWithCustomError( + shortfall, + "ZeroAddressNotAllowed", + ); + }); + + it("fails if called by a non-owner", async function () { + await expect(shortfall.connect(someone).updatePoolRegistry(poolRegistry.address)).to.be.rejectedWith( + "Ownable: caller is not the owner", + ); + }); + + it("emits PoolRegistryUpdated event", async function () { + const tx = shortfall.updatePoolRegistry(someone.address); + await expect(tx).to.emit(shortfall, "PoolRegistryUpdated").withArgs(poolRegistry.address, someone.address); + }); + }); - const auction = await shortfall.auctions(poolAddress); + describe("updateMinimumPoolBadDebt", async function () { + it("fails if called by a non permissioned account", async function () { + await expect(shortfall.connect(someone).updateMinimumPoolBadDebt(1)).to.be.reverted; + }); + + it("updates minimumPoolBadDebt in storage", async function () { + await shortfall.updateMinimumPoolBadDebt(1); + expect(await shortfall.minimumPoolBadDebt()).to.equal(1); + }); + + it("emits MinimumPoolBadDebtUpdated event", async function () { + const tx = shortfall.updateMinimumPoolBadDebt(1); + await expect(tx) + .to.emit(shortfall, "MinimumPoolBadDebtUpdated") + .withArgs(parseUnits(minimumPoolBadDebt, "18"), 1); + }); + }); - expect(auction.status).equal(1); - expect(auction.auctionType).equal(0); - expect(auction.seizedRiskFund).equal(parseUnits(riskFundBalance, 18)); + describe("waitForFirstBidder", async function () { + it("fails if called by a non permissioned account", async function () { + await expect(shortfall.connect(someone).updateWaitForFirstBidder(200)).to.be.reverted; + }); + + it("updates updateWaitForFirstBidder in storage", async function () { + await shortfall.updateWaitForFirstBidder(200); + expect(await shortfall.waitForFirstBidder()).to.equal(200); + }); + + it("emits WaitForFirstBidderUpdated event", async function () { + const tx = shortfall.updateWaitForFirstBidder(200); + if (!isTimeBased) { + await expect(tx).to.emit(shortfall, "WaitForFirstBidderUpdated").withArgs(100, 200); + } else { + await expect(tx).to.emit(shortfall, "WaitForFirstBidderUpdated").withArgs(300, 200); + } + }); + }); - const startBidBps = new BigNumber("10000000000").dividedBy("52000.68").dividedBy("11000").toFixed(2); - expect(auction.startBidBps.toString()).equal(new BigNumber(startBidBps).times(100).toString()); + describe("updateNextBidderBlockLimit", async function () { + it("fails if called by a non permissioned account", async function () { + await accessControlManager.revokeCallPermission( + shortfall.address, + "updateNextBidderBlockLimit(uint256)", + owner.address, + ); + await expect(shortfall.connect(someone).updateNextBidderBlockLimit(1)).to.be.reverted; + await accessControlManager.giveCallPermission( + shortfall.address, + "updateNextBidderBlockLimit(uint256)", + owner.address, + ); + }); + + it("updates nextBidderBlockLimit in storage", async function () { + await shortfall.updateNextBidderBlockLimit(100); + expect(await shortfall.nextBidderBlockLimit()).to.equal(100); + }); + + it("emits NextBidderBlockLimitUpdated event", async function () { + const tx = shortfall.updateNextBidderBlockLimit(110); + if (!isTimeBased) { + await expect(tx).to.emit(shortfall, "NextBidderBlockLimitUpdated").withArgs(100, 110); + } else { + await expect(tx).to.emit(shortfall, "NextBidderBlockLimitUpdated").withArgs(300, 110); + } + }); + }); }); - it("Should not be able to place bid lower max basis points", async function () { - const auction = await shortfall.auctions(poolAddress); - await expect(shortfall.placeBid(poolAddress, "10001", auction.startBlock)).to.be.revertedWith( - "basis points cannot be more than 10000", - ); - }); + describe("updateIncentiveBps", async function () { + it("fails if caller is not allowed", async function () { + await expect(shortfall.connect(someone).updateIncentiveBps(1)).to.be.reverted; + }); - it("Place bid", async function () { - const auction = await shortfall.auctions(poolAddress); + it("fails if new incentive BPS is set to 0", async function () { + await expect(shortfall.updateIncentiveBps(0)).to.be.revertedWith("incentiveBps must not be 0"); + }); - await mockDAI.approve(shortfall.address, parseUnits("10000", 18)); - await mockWBTC.approve(shortfall.address, parseUnits("2", 8)); - - const previousDaiBalance = await mockDAI.balanceOf(owner.address); - const previousWBTCBalance = await mockWBTC.balanceOf(owner.address); - - await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlock); - expect((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toNumber()).lt( - previousDaiBalance.div(parseUnits("1", 18)).toNumber(), - ); - expect((await mockWBTC.balanceOf(owner.address)).div(parseUnits("1", 8)).toNumber()).lt( - previousWBTCBalance.div(parseUnits("1", 8)).toNumber(), - ); - - let percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - let total = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - let amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); - let amountDeducted = new BigNumber(previousDaiBalance.div(parseUnits("1", 18)).toString()) - .minus((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toString()) - .toString(); - expect(amountDeducted).equal(amountToDeduct); - - percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - total = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "8").toString()); - amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); - amountDeducted = new BigNumber(previousWBTCBalance.toString()) - .minus((await mockWBTC.balanceOf(owner.address)).toString()) - .div(parseUnits("1", 8).toString()) - .toString(); - expect(amountDeducted).equal(amountToDeduct); + it("emits IncentiveBpsUpdated event", async function () { + const tx = shortfall.updateIncentiveBps(2000); + await expect(tx).to.emit(shortfall, "IncentiveBpsUpdated").withArgs(1000, 2000); + }); }); - it("Should not be able to start auction while on is ongoing", async function () { - await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("auction is on-going"); - }); + describe("placeBid", async function () { + beforeEach(async function () { + await setup(isTimeBased); + }); + let auctionStartBlock; + + async function startAuction() { + vDAI.badDebt.returns(parseUnits("10000", 18)); + await vDAI.setVariable("badDebt", parseUnits("10000", 18)); + vWBTC.badDebt.returns(parseUnits("2", 8)); + await vWBTC.setVariable("badDebt", parseUnits("2", 8)); + await shortfall.startAuction(poolAddress); + const auction = await shortfall.auctions(poolAddress); + auctionStartBlock = auction.startBlockOrTimestamp; + } - it("Should not be able to place bid lower than highest bid", async function () { - const auction = await shortfall.auctions(poolAddress); - await expect(shortfall.placeBid(poolAddress, "1200", auction.startBlock)).to.be.revertedWith( - "your bid is not the highest", - ); + it("fails if auction is not active", async function () { + await expect(shortfall.placeBid(poolAddress, "10000", 0)).to.be.revertedWith("no on-going auction"); + }); + + it("fails if auction is stale", async function () { + await startAuction(); + if (!isTimeBased) { + await mine(100); + } else { + await mine(300); + } + await expect(shortfall.placeBid(poolAddress, "10000", auctionStartBlock)).to.be.revertedWith( + "auction is stale, restart it", + ); + }); + + it("fails if bidBps is zero", async () => { + await startAuction(); + await expect(shortfall.placeBid(poolAddress, "0", auctionStartBlock)).to.be.revertedWith( + "basis points cannot be zero", + ); + }); + + it("fails if auctionStartBlock does not match the auction startBlock", async () => { + await startAuction(); + await mine(10); + let latestBlockOrTimestamp = (await ethers.provider.getBlock("latest")).number; + if (isTimeBased) { + latestBlockOrTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + } + await expect(shortfall.placeBid(poolAddress, "0", latestBlockOrTimestamp)).to.be.revertedWith( + "auction has been restarted", + ); + }); }); - it("Transfer back previous balance after second bid", async function () { - const auction = await shortfall.auctions(poolAddress); - const previousOwnerDaiBalance = await mockDAI.balanceOf(owner.address); + describe("LARGE_POOL_DEBT Scenario", async function () { + before(async function () { + await setup(isTimeBased); + }); + let startBlockNumber; - const percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - const totalVDai = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - const amountToDeductVDai = new BigNumber(totalVDai).times(percentageToDeduct).dividedBy(100).toString(); + it("Should have debt and reserve", async function () { + vDAI.badDebt.returns(parseUnits("1000", 18)); + vWBTC.badDebt.returns(parseUnits("1", 8)); - const previousOwnerWBtcBalance = await mockWBTC.balanceOf(owner.address); - const totalVBtc = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - const amountToDeductVBtc = new BigNumber(totalVBtc).times(percentageToDeduct).dividedBy(100).toString(); + expect(await fakeRiskFund.getPoolsBaseAssetReserves(comptroller.address)).equal( + parseUnits(riskFundBalance, 18).toString(), + ); - await shortfall.connect(bidder2).placeBid(poolAddress, "1800", auction.startBlock); + expect(await vDAI.badDebt()).equal(parseUnits("1000", 18)); + expect(await vWBTC.badDebt()).equal(parseUnits("1", 8)); + }); - expect(await mockDAI.balanceOf(owner.address)).to.be.equal( - previousOwnerDaiBalance.add(convertToUnit(amountToDeductVDai, 18)), - ); - expect(await mockWBTC.balanceOf(owner.address)).to.be.equal( - previousOwnerWBtcBalance.add(convertToUnit(amountToDeductVBtc, 18)), - ); - }); + it("Should not be able to start auction when bad debt is low", async function () { + vDAI.badDebt.returns(parseUnits("20", 18)); + vWBTC.badDebt.returns(parseUnits("0.01", 8)); - it("can't close ongoing auction", async function () { - await expect(shortfall.closeAuction(poolAddress)).to.be.revertedWith( - "waiting for next bidder. cannot close auction", - ); - }); + await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("pool bad debt is too low"); + }); - it("Close Auction", async function () { - const originalBalance = await mockDAI.balanceOf(bidder2.address); - await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); + it("can't restart when there is no ongoing auction", async function () { + await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith("no on-going auction"); + }); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, parseUnits(riskFundBalance, 18)); - fakeRiskFund.transferReserveForAuction.returns(parseUnits("10000", 18)); + it("Should not be able to close auction when there is no active auction", async function () { + await expect(shortfall.closeAuction(poolAddress)).to.be.revertedWith("no on-going auction"); + }); - await expect(shortfall.closeAuction(poolAddress)) - .to.emit(shortfall, "AuctionClosed") - .withArgs( - comptroller.address, - startBlockNumber, - bidder2.address, - 1800, - parseUnits("10000", 18), - [vDAI.address, vWBTC.address], - [parseUnits("1800", 18), "36000000"], + it("Should not be able to placeBid when there is no active auction", async function () { + vDAI.badDebt.returns(parseUnits("20", 18)); + vWBTC.badDebt.returns(parseUnits("0.01", 8)); + const auction = await shortfall.auctions(poolAddress); + + await expect(shortfall.placeBid(poolAddress, "1800", auction.startBlockOrTimestamp)).to.be.revertedWith( + "no on-going auction", ); + }); - const auction = await shortfall.auctions(poolAddress); - expect(auction.status).equal(2); + it("Start auction", async function () { + vDAI.badDebt.returns(parseUnits("10000", 18)); + await vDAI.setVariable("badDebt", parseUnits("10000", 18)); + vWBTC.badDebt.returns(parseUnits("2", 8)); + await vWBTC.setVariable("badDebt", parseUnits("2", 8)); + + await shortfall.startAuction(poolAddress); + const auction = await shortfall.auctions(poolAddress); + startBlockNumber = auction.startBlockOrTimestamp; + expect(auction.status).equal(1); + expect(auction.auctionType).equal(0); + expect(auction.seizedRiskFund).equal(parseUnits(riskFundBalance, 18)); + + const startBidBps = new BigNumber("10000000000").dividedBy("52000.68").dividedBy("11000").toFixed(2); + expect(auction.startBidBps.toString()).equal(new BigNumber(startBidBps).times(100).toString()); + }); - expect(vWBTC.badDebtRecovered).to.have.been.calledOnce; - expect(vWBTC.badDebtRecovered).to.have.been.calledWith("36000000"); + it("Should not be able to place bid lower max basis points", async function () { + const auction = await shortfall.auctions(poolAddress); + await expect(shortfall.placeBid(poolAddress, "10001", auction.startBlockOrTimestamp)).to.be.revertedWith( + "basis points cannot be more than 10000", + ); + }); - expect(vDAI.badDebtRecovered).to.have.been.calledOnce; - expect(vDAI.badDebtRecovered).to.have.been.calledWith(parseUnits("1800", 18)); - expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(auction.seizedRiskFund)); - }); - }); + it("Place bid", async function () { + const auction = await shortfall.auctions(poolAddress); - describe("LARGE_RISK_FUND Scenario", async function () { - before(setup); - let startBlockNumber; - it("Start auction", async function () { - vDAI.badDebt.returns(parseUnits("10000", 18)); - await vDAI.setVariable("badDebt", parseUnits("10000", 18)); - vWBTC.badDebt.returns(parseUnits("1", 8)); - await vWBTC.setVariable("badDebt", parseUnits("1", 8)); + await mockDAI.approve(shortfall.address, parseUnits("10000", 18)); + await mockWBTC.approve(shortfall.address, parseUnits("2", 8)); - riskFundBalance = "50000"; - fakeRiskFund.getPoolsBaseAssetReserves.returns(parseUnits(riskFundBalance, 18)); + const previousDaiBalance = await mockDAI.balanceOf(owner.address); + const previousWBTCBalance = await mockWBTC.balanceOf(owner.address); - const receipt = await shortfall.startAuction(poolAddress); - startBlockNumber = receipt.blockNumber; + await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); + expect((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toNumber()).lt( + previousDaiBalance.div(parseUnits("1", 18)).toNumber(), + ); + expect((await mockWBTC.balanceOf(owner.address)).div(parseUnits("1", 8)).toNumber()).lt( + previousWBTCBalance.div(parseUnits("1", 8)).toNumber(), + ); - const auction = await shortfall.auctions(poolAddress); - expect(auction.status).equal(1); - expect(auction.auctionType).equal(1); + let percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + let total = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + let amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); + let amountDeducted = new BigNumber(previousDaiBalance.div(parseUnits("1", 18)).toString()) + .minus((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toString()) + .toString(); + expect(amountDeducted).equal(amountToDeduct); + + percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + total = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "8").toString()); + amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); + amountDeducted = new BigNumber(previousWBTCBalance.toString()) + .minus((await mockWBTC.balanceOf(owner.address)).toString()) + .div(parseUnits("1", 8).toString()) + .toString(); + expect(amountDeducted).equal(amountToDeduct); + }); - const startBidBps = new BigNumber(new BigNumber("21000.34").plus(10000).times(1.1).times(100)).dividedBy( - riskFundBalance, - ); - expect(new BigNumber(startBidBps).times(riskFundBalance).dividedBy(100).toString()).equal( - new BigNumber(auction.seizedRiskFund.toString()).dividedBy(parseUnits("1", 18).toString()).toString(), - ); - }); + it("Should not be able to start auction while on is ongoing", async function () { + await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("auction is on-going"); + }); - it("Place bid", async function () { - const auction = await shortfall.auctions(poolAddress); + it("Should not be able to place bid lower than highest bid", async function () { + const auction = await shortfall.auctions(poolAddress); + await expect(shortfall.placeBid(poolAddress, "1200", auction.startBlockOrTimestamp)).to.be.revertedWith( + "your bid is not the highest", + ); + }); - await mockDAI.approve(shortfall.address, parseUnits("10000", 18)); - await mockWBTC.approve(shortfall.address, parseUnits("1", 8)); - - const previousDaiBalance = await mockDAI.balanceOf(owner.address); - const previousWBTCBalance = await mockWBTC.balanceOf(owner.address); - - await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlock); - expect((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toNumber()).lt( - previousDaiBalance.div(parseUnits("1", 18)).toNumber(), - ); - expect((await mockWBTC.balanceOf(owner.address)).div(parseUnits("1", 8)).toNumber()).lt( - previousWBTCBalance.div(parseUnits("1", 8)).toNumber(), - ); - - let percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - let total = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - let amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); - let amountDeducted = new BigNumber(previousDaiBalance.div(parseUnits("1", 18)).toString()) - .minus((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toString()) - .toString(); - expect(amountDeducted).equal(amountToDeduct); - - percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - total = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "8").toString()); - amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); - amountDeducted = new BigNumber(previousWBTCBalance.toString()) - .minus((await mockWBTC.balanceOf(owner.address)).toString()) - .div(parseUnits("1", 8).toString()) - .toString(); - expect(amountDeducted).equal(amountToDeduct); - }); + it("Transfer back previous balance after second bid", async function () { + const auction = await shortfall.auctions(poolAddress); + const previousOwnerDaiBalance = await mockDAI.balanceOf(owner.address); - it("Transfer back previous balance after second bid", async function () { - const auction = await shortfall.auctions(poolAddress); - const previousOwnerDaiBalance = await mockDAI.balanceOf(owner.address); + const percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + const totalVDai = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + const amountToDeductVDai = new BigNumber(totalVDai).times(percentageToDeduct).dividedBy(100).toString(); - const percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); - const totalVDai = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - const amountToDeductVDai = new BigNumber(totalVDai).times(percentageToDeduct).dividedBy(100).toString(); + const previousOwnerWBtcBalance = await mockWBTC.balanceOf(owner.address); + const totalVBtc = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + const amountToDeductVBtc = new BigNumber(totalVBtc).times(percentageToDeduct).dividedBy(100).toString(); - const previousOwnerWBtcBalance = await mockWBTC.balanceOf(owner.address); - const totalVBtc = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); - const amountToDeductVBtc = new BigNumber(totalVBtc).times(percentageToDeduct).dividedBy(100).toString(); + await shortfall.connect(bidder2).placeBid(poolAddress, "1800", auction.startBlockOrTimestamp); - await shortfall.connect(bidder2).placeBid(poolAddress, "1800", auction.startBlock); + expect(await mockDAI.balanceOf(owner.address)).to.be.equal( + previousOwnerDaiBalance.add(convertToUnit(amountToDeductVDai, 18)), + ); + expect(await mockWBTC.balanceOf(owner.address)).to.be.equal( + previousOwnerWBtcBalance.add(convertToUnit(amountToDeductVBtc, 18)), + ); + }); - expect(await mockDAI.balanceOf(owner.address)).to.be.equal( - previousOwnerDaiBalance.add(convertToUnit(amountToDeductVDai, 18)), - ); - expect(await mockWBTC.balanceOf(owner.address)).to.be.equal( - previousOwnerWBtcBalance.add(convertToUnit(amountToDeductVBtc, 18)), - ); + it("can't close ongoing auction", async function () { + await expect(shortfall.closeAuction(poolAddress)).to.be.revertedWith( + "waiting for next bidder. cannot close auction", + ); + }); + + it("Close Auction", async function () { + const originalBalance = await mockDAI.balanceOf(bidder2.address); + await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); + + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, parseUnits(riskFundBalance, 18)); + fakeRiskFund.transferReserveForAuction.returns(parseUnits("10000", 18)); + + await expect(shortfall.closeAuction(poolAddress)) + .to.emit(shortfall, "AuctionClosed") + .withArgs( + comptroller.address, + startBlockNumber, + bidder2.address, + 1800, + parseUnits("10000", 18), + [vDAI.address, vWBTC.address], + [parseUnits("1800", 18), "36000000"], + ); + + const auction = await shortfall.auctions(poolAddress); + expect(auction.status).equal(2); + + expect(vWBTC.badDebtRecovered).to.have.been.calledOnce; + expect(vWBTC.badDebtRecovered).to.have.been.calledWith("36000000"); + + expect(vDAI.badDebtRecovered).to.have.been.calledOnce; + expect(vDAI.badDebtRecovered).to.have.been.calledWith(parseUnits("1800", 18)); + expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(auction.seizedRiskFund)); + }); }); - it("Close Auction", async function () { - const originalBalance = await mockDAI.balanceOf(bidder2.address); - let auction = await shortfall.auctions(poolAddress); + describe("LARGE_RISK_FUND Scenario", async function () { + before(async function () { + await setup(isTimeBased); + }); + let startBlockNumber; + it("Start auction", async function () { + vDAI.badDebt.returns(parseUnits("10000", 18)); + await vDAI.setVariable("badDebt", parseUnits("10000", 18)); + vWBTC.badDebt.returns(parseUnits("1", 8)); + await vWBTC.setVariable("badDebt", parseUnits("1", 8)); - await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); + const newRiskFundBalance = "50000"; + fakeRiskFund.getPoolsBaseAssetReserves.returns(parseUnits(newRiskFundBalance, 18)); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); - fakeRiskFund.transferReserveForAuction.returns("6138067320000000000000"); + await shortfall.startAuction(poolAddress); + startBlockNumber = (await shortfall.auctions(poolAddress)).startBlockOrTimestamp; - await expect(shortfall.closeAuction(poolAddress)) - .to.emit(shortfall, "AuctionClosed") - .withArgs( - comptroller.address, - startBlockNumber, - bidder2.address, - 1800, - "6138067320000000000000", - [vDAI.address, vWBTC.address], - [parseUnits("10000", 18), "100000000"], + const auction = await shortfall.auctions(poolAddress); + expect(auction.status).equal(1); + expect(auction.auctionType).equal(1); + + const startBidBps = new BigNumber(new BigNumber("21000.34").plus(10000).times(1.1).times(100)).dividedBy( + newRiskFundBalance, ); - auction = await shortfall.auctions(poolAddress); - expect(auction.status).equal(2); + expect(new BigNumber(startBidBps).times(newRiskFundBalance).dividedBy(100).toString()).equal( + new BigNumber(auction.seizedRiskFund.toString()).dividedBy(parseUnits("1", 18).toString()).toString(), + ); + }); - expect(vWBTC.badDebtRecovered).to.have.been.calledTwice; - expect(vWBTC.badDebtRecovered).to.have.been.calledWith(parseUnits("1", 8)); + it("Place bid", async function () { + const auction = await shortfall.auctions(poolAddress); - expect(vDAI.badDebtRecovered).to.have.been.calledTwice; - expect(vDAI.badDebtRecovered).to.have.been.calledWith(parseUnits("10000", 18)); - const riskFundBidAmount = auction.seizedRiskFund.mul(auction.highestBidBps).div(10000); - expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(riskFundBidAmount)); - }); - }); + await mockDAI.approve(shortfall.address, parseUnits("10000", 18)); + await mockWBTC.approve(shortfall.address, parseUnits("1", 8)); - describe("Restart Auction", async function () { - beforeEach(setup); + const previousDaiBalance = await mockDAI.balanceOf(owner.address); + const previousWBTCBalance = await mockWBTC.balanceOf(owner.address); - it("Can't restart auction early ", async function () { - vDAI.badDebt.returns(parseUnits("10000", 18)); - await vDAI.setVariable("badDebt", parseUnits("10000", 18)); - vWBTC.badDebt.returns(parseUnits("2", 8)); - await vWBTC.setVariable("badDebt", parseUnits("2", 8)); + await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); + expect((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toNumber()).lt( + previousDaiBalance.div(parseUnits("1", 18)).toNumber(), + ); + expect((await mockWBTC.balanceOf(owner.address)).div(parseUnits("1", 8)).toNumber()).lt( + previousWBTCBalance.div(parseUnits("1", 8)).toNumber(), + ); - await shortfall.startAuction(poolAddress); - await mine(5); - await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith( - "you need to wait for more time for first bidder", - ); - }); + let percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + let total = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + let amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); + let amountDeducted = new BigNumber(previousDaiBalance.div(parseUnits("1", 18)).toString()) + .minus((await mockDAI.balanceOf(owner.address)).div(parseUnits("1", 18)).toString()) + .toString(); + expect(amountDeducted).equal(amountToDeduct); + + percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + total = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "8").toString()); + amountToDeduct = new BigNumber(total).times(percentageToDeduct).dividedBy(100).toString(); + amountDeducted = new BigNumber(previousWBTCBalance.toString()) + .minus((await mockWBTC.balanceOf(owner.address)).toString()) + .div(parseUnits("1", 8).toString()) + .toString(); + expect(amountDeducted).equal(amountToDeduct); + }); + + it("Transfer back previous balance after second bid", async function () { + const auction = await shortfall.auctions(poolAddress); + const previousOwnerDaiBalance = await mockDAI.balanceOf(owner.address); - it("Can restart auction", async function () { - vDAI.badDebt.returns(parseUnits("1000", 18)); - await vDAI.setVariable("badDebt", parseUnits("1000", 18)); - vWBTC.badDebt.returns(parseUnits("1", 8)); - await vWBTC.setVariable("badDebt", parseUnits("1", 8)); + const percentageToDeduct = new BigNumber(auction.startBidBps.toString()).dividedBy(100); + const totalVDai = new BigNumber((await vDAI.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + const amountToDeductVDai = new BigNumber(totalVDai).times(percentageToDeduct).dividedBy(100).toString(); - const receipt = await shortfall.startAuction(poolAddress); + const previousOwnerWBtcBalance = await mockWBTC.balanceOf(owner.address); + const totalVBtc = new BigNumber((await vWBTC.badDebt()).toString()).dividedBy(parseUnits("1", "18").toString()); + const amountToDeductVBtc = new BigNumber(totalVBtc).times(percentageToDeduct).dividedBy(100).toString(); - await mine(100); + await shortfall.connect(bidder2).placeBid(poolAddress, "1800", auction.startBlockOrTimestamp); - await expect(shortfall.restartAuction(poolAddress)) - .to.emit(shortfall, "AuctionRestarted") - .withArgs(poolAddress, receipt.blockNumber); + expect(await mockDAI.balanceOf(owner.address)).to.be.equal( + previousOwnerDaiBalance.add(convertToUnit(amountToDeductVDai, 18)), + ); + expect(await mockWBTC.balanceOf(owner.address)).to.be.equal( + previousOwnerWBtcBalance.add(convertToUnit(amountToDeductVBtc, 18)), + ); + }); + + it("Close Auction", async function () { + const originalBalance = await mockDAI.balanceOf(bidder2.address); + let auction = await shortfall.auctions(poolAddress); + + await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); + + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + fakeRiskFund.transferReserveForAuction.returns("6138067320000000000000"); + + await expect(shortfall.closeAuction(poolAddress)) + .to.emit(shortfall, "AuctionClosed") + .withArgs( + comptroller.address, + startBlockNumber, + bidder2.address, + 1800, + "6138067320000000000000", + [vDAI.address, vWBTC.address], + [parseUnits("10000", 18), "100000000"], + ); + auction = await shortfall.auctions(poolAddress); + expect(auction.status).equal(2); + + expect(vWBTC.badDebtRecovered).to.have.been.calledTwice; + expect(vWBTC.badDebtRecovered).to.have.been.calledWith(parseUnits("1", 8)); + + expect(vDAI.badDebtRecovered).to.have.been.calledTwice; + expect(vDAI.badDebtRecovered).to.have.been.calledWith(parseUnits("10000", 18)); + const riskFundBidAmount = auction.seizedRiskFund.mul(auction.highestBidBps).div(10000); + expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(riskFundBidAmount)); + }); }); - it("Cannot restart auction after a bid is placed", async function () { - vDAI.badDebt.returns(parseUnits("1000", 18)); - await vDAI.setVariable("badDebt", parseUnits("1000", 18)); - vWBTC.badDebt.returns(parseUnits("1", 8)); - await vWBTC.setVariable("badDebt", parseUnits("1", 8)); + describe("Restart Auction", async function () { + beforeEach(async function () { + await setup(isTimeBased); + }); - await shortfall.startAuction(poolAddress); - const auction = await shortfall.auctions(poolAddress); + it("Can't restart auction early ", async function () { + vDAI.badDebt.returns(parseUnits("10000", 18)); + await vDAI.setVariable("badDebt", parseUnits("10000", 18)); + vWBTC.badDebt.returns(parseUnits("2", 8)); + await vWBTC.setVariable("badDebt", parseUnits("2", 8)); - await mockDAI.approve(shortfall.address, parseUnits("50000", 18)); - await mockWBTC.approve(shortfall.address, parseUnits("50000", 8)); + await shortfall.startAuction(poolAddress); + await mine(5); + await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith( + "you need to wait for more time for first bidder", + ); + }); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + it("Can restart auction", async function () { + vDAI.badDebt.returns(parseUnits("1000", 18)); + await vDAI.setVariable("badDebt", parseUnits("1000", 18)); + vWBTC.badDebt.returns(parseUnits("1", 8)); + await vWBTC.setVariable("badDebt", parseUnits("1", 8)); - await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlock); + await shortfall.startAuction(poolAddress); + const startBlockNumberOrTimestamp = (await shortfall.auctions(poolAddress)).startBlockOrTimestamp; - await mine(100); + if (!isTimeBased) { + await mine(100); + } else { + await mine(300); + } - await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith( - "you need to wait for more time for first bidder", - ); - // Close out auction created for this test case - await mine(10); - await shortfall.closeAuction(poolAddress); - }); + await expect(shortfall.restartAuction(poolAddress)) + .to.emit(shortfall, "AuctionRestarted") + .withArgs(poolAddress, startBlockNumberOrTimestamp); + }); - it("Cannot restart auction if auctions paused", async function () { - vDAI.badDebt.returns(parseUnits("1000", 18)); - await vDAI.setVariable("badDebt", parseUnits("1000", 18)); - vWBTC.badDebt.returns(parseUnits("1", 8)); - await vWBTC.setVariable("badDebt", parseUnits("1", 8)); + it("Cannot restart auction after a bid is placed", async function () { + vDAI.badDebt.returns(parseUnits("1000", 18)); + await vDAI.setVariable("badDebt", parseUnits("1000", 18)); + vWBTC.badDebt.returns(parseUnits("1", 8)); + await vWBTC.setVariable("badDebt", parseUnits("1", 8)); - await shortfall.startAuction(poolAddress); - const auction = await shortfall.auctions(poolAddress); + await shortfall.startAuction(poolAddress); + const auction = await shortfall.auctions(poolAddress); - await mockDAI.approve(shortfall.address, parseUnits("50000", 18)); - await mockWBTC.approve(shortfall.address, parseUnits("50000", 8)); + await mockDAI.approve(shortfall.address, parseUnits("50000", 18)); + await mockWBTC.approve(shortfall.address, parseUnits("50000", 8)); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); - await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlock); + await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); - await mine(100); - await shortfall.pauseAuctions(); - await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith("auctions are paused"); - // Close out auction created for this test case - await mine(10); - await shortfall.closeAuction(poolAddress); - await shortfall.resumeAuctions(); - }); - }); + if (!isTimeBased) { + await mine(100); + } else { + await mine(300); + } - describe("Auctions can be enabled and disabled", async function () { - it("fails if called by a non permissioned account", async function () { - await expect(shortfall.connect(someone).pauseAuctions()).to.be.reverted; + await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith( + "you need to wait for more time for first bidder", + ); + // Close out auction created for this test case + await mine(10); + await shortfall.closeAuction(poolAddress); + }); + + it("Cannot restart auction if auctions paused", async function () { + vDAI.badDebt.returns(parseUnits("1000", 18)); + await vDAI.setVariable("badDebt", parseUnits("1000", 18)); + vWBTC.badDebt.returns(parseUnits("1", 8)); + await vWBTC.setVariable("badDebt", parseUnits("1", 8)); + + await shortfall.startAuction(poolAddress); + const auction = await shortfall.auctions(poolAddress); + + await mockDAI.approve(shortfall.address, parseUnits("50000", 18)); + await mockWBTC.approve(shortfall.address, parseUnits("50000", 8)); + + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + + await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); + + if (!isTimeBased) { + await mine(100); + } else { + await mine(300); + } + await shortfall.pauseAuctions(); + await expect(shortfall.restartAuction(poolAddress)).to.be.revertedWith("auctions are paused"); + // Close out auction created for this test case + await mine(10); + await shortfall.closeAuction(poolAddress); + await shortfall.resumeAuctions(); + }); }); - it("can close current auction but not start new one when they are paused", async function () { - vDAI.badDebt.returns(parseUnits("10000", 18)); - await vDAI.setVariable("badDebt", parseUnits("10000", 18)); - vWBTC.badDebt.returns(parseUnits("2", 8)); - await vWBTC.setVariable("badDebt", parseUnits("2", 8)); + describe("Auctions can be enabled and disabled", async function () { + it("fails if called by a non permissioned account", async function () { + await expect(shortfall.connect(someone).pauseAuctions()).to.be.reverted; + }); - await shortfall.startAuction(poolAddress); - const auction = await shortfall.auctions(poolAddress); + it("can close current auction but not start new one when they are paused", async function () { + vDAI.badDebt.returns(parseUnits("10000", 18)); + await vDAI.setVariable("badDebt", parseUnits("10000", 18)); + vWBTC.badDebt.returns(parseUnits("2", 8)); + await vWBTC.setVariable("badDebt", parseUnits("2", 8)); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + await shortfall.startAuction(poolAddress); + const auction = await shortfall.auctions(poolAddress); + + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); - await expect(shortfall.connect(owner).pauseAuctions()) - .to.emit(shortfall, "AuctionsPaused") - .withArgs(owner.address); + await expect(shortfall.connect(owner).pauseAuctions()) + .to.emit(shortfall, "AuctionsPaused") + .withArgs(owner.address); - await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlock); - // Close out auction created for this test case - await mine(10); - await expect(shortfall.closeAuction(poolAddress)); - await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("Auctions are paused"); + await shortfall.placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); + // Close out auction created for this test case + await mine(10); + await expect(shortfall.closeAuction(poolAddress)); + await expect(shortfall.startAuction(poolAddress)).to.be.revertedWith("Auctions are paused"); + }); }); }); -}); -describe("Shortfall: Deflationary token Scenario", async function () { - before(async () => { - await mine(1000); - await setup(); - }); - let startBlockNumber; - it("Start auction", async function () { - vDAI.badDebt.returns("0"); - vWBTC.badDebt.returns("0"); - vFloki.badDebt.returns(parseUnits("100", 18)); - await vFloki.setVariable("badDebt", parseUnits("100", 18)); + describe(`${description}Shortfall: Deflationary token Scenario`, async function () { + let startBlockNumberOrTimestamp; - riskFundBalance = "500"; - fakeRiskFund.getPoolsBaseAssetReserves.returns(parseUnits(riskFundBalance, 18)); + before(async () => { + await mine(1000); + await setup(isTimeBased); + }); - await shortfall.connect(owner).updateMinimumPoolBadDebt(convertToUnit(10, 18)); + it("Start auction", async function () { + vDAI.badDebt.returns("0"); + vWBTC.badDebt.returns("0"); + vFloki.badDebt.returns(parseUnits("100", 18)); + await vFloki.setVariable("badDebt", parseUnits("100", 18)); - const receipt = await shortfall.startAuction(poolAddress); - startBlockNumber = receipt.blockNumber; + const newRiskFundBalance = "500"; + fakeRiskFund.getPoolsBaseAssetReserves.returns(parseUnits(newRiskFundBalance, 18)); - const auction = await shortfall.auctions(poolAddress); - expect(auction.status).equal(1); - expect(auction.auctionType).equal(1); - }); + await shortfall.connect(owner).updateMinimumPoolBadDebt(convertToUnit(10, 18)); - it("Place bid", async function () { - const auction = await shortfall.auctions(poolAddress); - await mockFloki.approve(shortfall.address, parseUnits("100", 18)); + await shortfall.startAuction(poolAddress); + startBlockNumberOrTimestamp = (await shortfall.auctions(poolAddress)).startBlockOrTimestamp; - const tx = await shortfall.connect(bidder1).placeBid(poolAddress, auction.startBidBps, auction.startBlock); + const auction = await shortfall.auctions(poolAddress); + expect(auction.status).equal(1); + expect(auction.auctionType).equal(1); + }); - await expect(tx).to.changeTokenBalance(mockFloki, bidder1.address, "-100000000000000000000"); - await expect(tx).to.changeTokenBalance(mockFloki, shortfall.address, convertToUnit("99", 18)); - }); + it("Place bid", async function () { + const auction = await shortfall.auctions(poolAddress); + await mockFloki.approve(shortfall.address, parseUnits("100", 18)); - it("Transfer back previous balance after second bid", async function () { - const auction = await shortfall.auctions(poolAddress); - const tx = await shortfall.connect(bidder2).placeBid(poolAddress, "120", auction.startBlock); + const tx = await shortfall + .connect(bidder1) + .placeBid(poolAddress, auction.startBidBps, auction.startBlockOrTimestamp); - await expect(tx).to.changeTokenBalance(mockFloki, bidder1.address, "98010000000000000000"); - await expect(tx).to.changeTokenBalance(mockFloki, bidder2.address, "-100000000000000000000"); - await expect(tx).to.changeTokenBalance(mockFloki, shortfall.address, "0"); - }); + await expect(tx).to.changeTokenBalance(mockFloki, bidder1.address, "-100000000000000000000"); + await expect(tx).to.changeTokenBalance(mockFloki, shortfall.address, convertToUnit("99", 18)); + }); + + it("Transfer back previous balance after second bid", async function () { + const auction = await shortfall.auctions(poolAddress); + const tx = await shortfall.connect(bidder2).placeBid(poolAddress, "120", auction.startBlockOrTimestamp); + + await expect(tx).to.changeTokenBalance(mockFloki, bidder1.address, "98010000000000000000"); + await expect(tx).to.changeTokenBalance(mockFloki, bidder2.address, "-100000000000000000000"); + await expect(tx).to.changeTokenBalance(mockFloki, shortfall.address, "0"); + }); - it("Close Auction", async function () { - const originalBalance = await mockDAI.balanceOf(bidder2.address); - let auction = await shortfall.auctions(poolAddress); + it("Close Auction", async function () { + const originalBalance = await mockDAI.balanceOf(bidder2.address); + let auction = await shortfall.auctions(poolAddress); - await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); + await mine((await shortfall.nextBidderBlockLimit()).toNumber() + 2); - // simulate transferReserveForAuction - await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); + // simulate transferReserveForAuction + await mockDAI.transfer(shortfall.address, auction.seizedRiskFund); - const tx = await shortfall.closeAuction(poolAddress); - await expect(tx) - .to.emit(shortfall, "AuctionClosed") - .withArgs( - comptroller.address, - startBlockNumber, - bidder2.address, - 120, - "6138067320000000000000", - [vDAI.address, vWBTC.address, vFloki.address], - ["0", "0", "98010000000000000000"], - ); + const tx = await shortfall.closeAuction(poolAddress); + await expect(tx) + .to.emit(shortfall, "AuctionClosed") + .withArgs( + comptroller.address, + startBlockNumberOrTimestamp, + bidder2.address, + 120, + "6138067320000000000000", + [vDAI.address, vWBTC.address, vFloki.address], + ["0", "0", "98010000000000000000"], + ); - await expect(tx).to.changeTokenBalance(mockFloki, vFloki.address, "98010000000000000000"); + await expect(tx).to.changeTokenBalance(mockFloki, vFloki.address, "98010000000000000000"); - auction = await shortfall.auctions(poolAddress); - expect(auction.status).equal(2); + auction = await shortfall.auctions(poolAddress); + expect(auction.status).equal(2); - expect(vFloki.badDebtRecovered).to.have.been.calledWith("98010000000000000000"); + expect(vFloki.badDebtRecovered).to.have.been.calledWith("98010000000000000000"); - const riskFundBidAmount = auction.seizedRiskFund.mul(auction.highestBidBps).div(10000); + const riskFundBidAmount = auction.seizedRiskFund.mul(auction.highestBidBps).div(10000); - expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(riskFundBidAmount)); + expect(await mockDAI.balanceOf(bidder2.address)).to.be.equal(originalBalance.add(riskFundBidAmount)); + }); }); -}); +} diff --git a/tests/hardhat/Tokens/accrueInterestTest.ts b/tests/hardhat/Tokens/accrueInterestTest.ts index 5a146b336..bfeee0108 100644 --- a/tests/hardhat/Tokens/accrueInterestTest.ts +++ b/tests/hardhat/Tokens/accrueInterestTest.ts @@ -7,15 +7,17 @@ import { parseUnits } from "ethers/lib/utils"; import { InterestRateModel, VTokenHarness } from "../../../typechain"; import { vTokenTestFixture } from "../util/TokenTestHelpers"; +import { getDescription } from "../util/descriptionHelpers"; const { expect } = chai; chai.use(smock.matchers); const blockNumber = 2e7; +const blockTimestamp = 1700033367; const borrowIndex = parseUnits("1", 18).toBigInt(); const borrowRate = parseUnits("0.000001", 18).toBigInt(); -async function pretendBlock( +async function pretendBlockOrTimestamp( vToken: VTokenHarness, accrualBlock: BigNumberish = blockNumber, deltaBlocks: BigNumberish = 1, @@ -38,110 +40,121 @@ async function preAccrue({ await vToken.harnessExchangeRateDetails(0, 0, 0); } -describe("VToken", () => { - let vToken: VTokenHarness; - let interestRateModel: FakeContract; - - beforeEach(async () => { - const contracts = await loadFixture(vTokenTestFixture); - ({ vToken, interestRateModel } = contracts); - await vToken.setReduceReservesBlockDelta(parseUnits("4", 10)); - }); - - beforeEach(async () => { - await preAccrue({ vToken, interestRateModel }); - }); - - describe("accrueInterest", () => { - it("reverts if the interest rate is absurdly high", async () => { - await pretendBlock(vToken, blockNumber, 1); - expect(await vToken.getBorrowRateMaxMantissa()).to.equal(parseUnits("0.000005", 18)); // 0.0005% per block - interestRateModel.getBorrowRate.returns(parseUnits("0.00001", 18)); // 0.0010% per block - await expect(vToken.accrueInterest()).to.be.revertedWith("borrow rate is absurdly high"); - }); - - it("fails if new borrow rate calculation fails", async () => { - await pretendBlock(vToken, blockNumber, 1); - interestRateModel.getBorrowRate.reverts("Oups"); - await expect(vToken.accrueInterest()).to.be.reverted; // With("INTEREST_RATE_MODEL_ERROR"); - }); - - it("fails if simple interest factor calculation fails", async () => { - await pretendBlock(vToken, blockNumber, parseUnits("5", 70)); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); - }); - - it("fails if new borrow index calculation fails", async () => { - await pretendBlock(vToken, blockNumber, parseUnits("5", 60)); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); - }); - - it("fails if new borrow interest index calculation fails", async () => { - await pretendBlock(vToken); - await vToken.harnessSetBorrowIndex(constants.MaxUint256); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); - }); +function getBlockNumberOrTimestamp(isTimeBased: boolean) { + if (!isTimeBased) { + return blockNumber; + } + return blockTimestamp; +} - it("fails if interest accumulated calculation fails", async () => { - await vToken.harnessExchangeRateDetails(0, constants.MaxUint256, 0); - await pretendBlock(vToken); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); - }); +for (const isTimeBased of [false, true]) { + const description = getDescription(isTimeBased); - it("fails if new total borrows calculation fails", async () => { - interestRateModel.getBorrowRate.returns("1"); - await pretendBlock(vToken); - await vToken.harnessExchangeRateDetails(0, constants.MaxUint256, 0); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); - }); + describe(`${description}VToken`, () => { + let vToken: VTokenHarness; + let interestRateModel: FakeContract; - it("fails if interest accumulated for reserves calculation fails", async () => { - interestRateModel.getBorrowRate.returns(parseUnits("0.000001", 18)); - await vToken.harnessExchangeRateDetails(0, parseUnits("1", 30), constants.MaxUint256); - await vToken.harnessSetReserveFactorFresh(parseUnits("1", 10)); - await pretendBlock(vToken, blockNumber, parseUnits("5", 20)); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + beforeEach(async () => { + const contracts = await loadFixture(vTokenTestFixture); + ({ vToken, interestRateModel } = contracts); + await vToken.setReduceReservesBlockDelta(parseUnits("4", 10)); }); - it("fails if new total reserves calculation fails", async () => { - interestRateModel.getBorrowRate.returns("1"); - await vToken.harnessExchangeRateDetails(0, parseUnits("1", 56), constants.MaxUint256); - await vToken.harnessSetReserveFactorFresh(parseUnits("1", 17)); - await pretendBlock(vToken); - await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + beforeEach(async () => { + await preAccrue({ vToken, interestRateModel }); }); - it("succeeds and saves updated values in storage on success", async () => { - const startingTotalBorrows = parseUnits("1", 22); - const startingTotalReserves = parseUnits("1", 20); - const reserveFactor = parseUnits("1", 17); - - await vToken.harnessExchangeRateDetails(0, startingTotalBorrows, startingTotalReserves); - await vToken.harnessSetReserveFactorFresh(reserveFactor); - await pretendBlock(vToken); - - const expectedAccrualBlockNumber = blockNumber + 1; - const expectedBorrowIndex = BigNumber.from(borrowIndex).add( - BigNumber.from(borrowIndex).mul(borrowRate).div(parseUnits("1", 18)), - ); - const expectedTotalBorrows = startingTotalBorrows.add( - startingTotalBorrows.mul(borrowRate).div(parseUnits("1", 18)), - ); - const expectedTotalReserves = startingTotalReserves.add( - startingTotalBorrows.mul(borrowRate).mul(reserveFactor).div(parseUnits("1", 36)), - ); - - const receipt = await vToken.accrueInterest(); - const expectedInterestAccumulated = expectedTotalBorrows.sub(startingTotalBorrows); - - await expect(receipt) - .to.emit(vToken, "AccrueInterest") - .withArgs(0, expectedInterestAccumulated, expectedBorrowIndex, expectedTotalBorrows); - - expect(await vToken.accrualBlockNumber()).to.equal(expectedAccrualBlockNumber); - expect(await vToken.borrowIndex()).to.equal(expectedBorrowIndex); - expect(await vToken.totalBorrows()).to.equal(expectedTotalBorrows); - expect(await vToken.totalReserves()).to.equal(expectedTotalReserves); + describe("accrueInterest", () => { + it("reverts if the interest rate is absurdly high", async () => { + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + expect(await vToken.getBorrowRateMaxMantissa()).to.equal(parseUnits("0.000005", 18)); // 0.0005% per block + interestRateModel.getBorrowRate.returns(parseUnits("0.00001", 18)); // 0.0010% per block + await expect(vToken.accrueInterest()).to.be.revertedWith("borrow rate is absurdly high"); + }); + + it("fails if new borrow rate calculation fails", async () => { + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + interestRateModel.getBorrowRate.reverts("Oups"); + await expect(vToken.accrueInterest()).to.be.reverted; // With("INTEREST_RATE_MODEL_ERROR"); + }); + + it("fails if simple interest factor calculation fails", async () => { + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), parseUnits("5", 70)); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if new borrow index calculation fails", async () => { + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), parseUnits("5", 60)); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if new borrow interest index calculation fails", async () => { + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + await vToken.harnessSetBorrowIndex(constants.MaxUint256); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if interest accumulated calculation fails", async () => { + await vToken.harnessExchangeRateDetails(0, constants.MaxUint256, 0); + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if new total borrows calculation fails", async () => { + interestRateModel.getBorrowRate.returns("1"); + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + await vToken.harnessExchangeRateDetails(0, constants.MaxUint256, 0); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if interest accumulated for reserves calculation fails", async () => { + interestRateModel.getBorrowRate.returns(parseUnits("0.000001", 18)); + await vToken.harnessExchangeRateDetails(0, parseUnits("1", 30), constants.MaxUint256); + await vToken.harnessSetReserveFactorFresh(parseUnits("1", 10)); + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), parseUnits("5", 20)); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("fails if new total reserves calculation fails", async () => { + interestRateModel.getBorrowRate.returns("1"); + await vToken.harnessExchangeRateDetails(0, parseUnits("1", 56), constants.MaxUint256); + await vToken.harnessSetReserveFactorFresh(parseUnits("1", 17)); + await pretendBlockOrTimestamp(vToken); + await expect(vToken.accrueInterest()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); + }); + + it("succeeds and saves updated values in storage on success", async () => { + const startingTotalBorrows = parseUnits("1", 22); + const startingTotalReserves = parseUnits("1", 20); + const reserveFactor = parseUnits("1", 17); + + await vToken.harnessExchangeRateDetails(0, startingTotalBorrows, startingTotalReserves); + await vToken.harnessSetReserveFactorFresh(reserveFactor); + await pretendBlockOrTimestamp(vToken, getBlockNumberOrTimestamp(isTimeBased), 1); + + const expectedAccrualBlockNumberOrTimestamp = getBlockNumberOrTimestamp(isTimeBased) + 1; + const expectedBorrowIndex = BigNumber.from(borrowIndex).add( + BigNumber.from(borrowIndex).mul(borrowRate).div(parseUnits("1", 18)), + ); + const expectedTotalBorrows = startingTotalBorrows.add( + startingTotalBorrows.mul(borrowRate).div(parseUnits("1", 18)), + ); + const expectedTotalReserves = startingTotalReserves.add( + startingTotalBorrows.mul(borrowRate).mul(reserveFactor).div(parseUnits("1", 36)), + ); + + const receipt = await vToken.accrueInterest(); + const expectedInterestAccumulated = expectedTotalBorrows.sub(startingTotalBorrows); + + await expect(receipt) + .to.emit(vToken, "AccrueInterest") + .withArgs(0, expectedInterestAccumulated, expectedBorrowIndex, expectedTotalBorrows); + + expect(await vToken.accrualBlockNumber()).to.equal(expectedAccrualBlockNumberOrTimestamp); + expect(await vToken.borrowIndex()).to.equal(expectedBorrowIndex); + expect(await vToken.totalBorrows()).to.equal(expectedTotalBorrows); + expect(await vToken.totalReserves()).to.equal(expectedTotalReserves); + }); }); }); -}); +} diff --git a/tests/hardhat/UpgradedVToken.ts b/tests/hardhat/UpgradedVToken.ts index 613f8c7d2..5855a2952 100644 --- a/tests/hardhat/UpgradedVToken.ts +++ b/tests/hardhat/UpgradedVToken.ts @@ -3,6 +3,7 @@ import { expect } from "chai"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; +import { BSC_BLOCKS_PER_YEAR } from "../../helpers/deploymentConfig"; import { convertToUnit } from "../../helpers/utils"; import { AccessControlManager, @@ -14,6 +15,7 @@ import { UpgradeableBeacon, } from "../../typechain"; import { deployVTokenBeacon, makeVToken } from "./util/TokenTestHelpers"; +import { getDescription } from "./util/descriptionHelpers"; // Disable a warning about mixing beacons and transparent proxies upgrades.silenceWarnings(); @@ -24,92 +26,109 @@ let comptroller1Proxy: Comptroller; let mockWBTC: MockToken; let fakeAccessControlManager: FakeContract; -describe("UpgradedVToken: Tests", function () { - /** - * Deploying required contracts along with the poolRegistry. - */ - before(async function () { - const [root] = await ethers.getSigners(); - - fakeAccessControlManager = await smock.fake("AccessControlManager"); - fakeAccessControlManager.isAllowedToCall.returns(true); - - const PoolRegistry = await ethers.getContractFactory("PoolRegistry"); - poolRegistry = (await upgrades.deployProxy(PoolRegistry, [fakeAccessControlManager.address])) as PoolRegistry; - - // Deploy Mock Tokens - const MockWBTC = await ethers.getContractFactory("MockToken"); - mockWBTC = await MockWBTC.deploy("Bitcoin", "BTC", 8); - - const _closeFactor = convertToUnit(0.05, 18); - const _liquidationIncentive = convertToUnit(1, 18); - const _minLiquidatableCollateral = convertToUnit(100, 18); - const maxLoopsLimit = 150; - - // Deploy Price Oracle - const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); - const priceOracle = await MockPriceOracle.deploy(); - - const btcPrice = "21000.34"; - - await priceOracle.setPrice(mockWBTC.address, convertToUnit(btcPrice, 28)); - - const Comptroller = await ethers.getContractFactory("Comptroller"); - const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); - - comptroller1Proxy = (await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ - maxLoopsLimit, - fakeAccessControlManager.address, - ])) as Comptroller; - await comptroller1Proxy.setPriceOracle(priceOracle.address); - - // Registering the first pool - await poolRegistry.addPool( - "Pool 1", - comptroller1Proxy.address, - _closeFactor, - _liquidationIncentive, - _minLiquidatableCollateral, - ); - - vTokenBeacon = await deployVTokenBeacon(); - const vWBTC = await makeVToken({ - underlying: mockWBTC.address, - comptroller: comptroller1Proxy.address, - accessControlManager: fakeAccessControlManager.address, - admin: root.address, - beacon: vTokenBeacon, +for (const isTimeBased of [false, true]) { + const description = getDescription(isTimeBased); + const slotsPerYear = isTimeBased ? 0 : BSC_BLOCKS_PER_YEAR; + + describe(`${description}UpgradedVToken: Tests`, function () { + /** + * Deploying required contracts along with the poolRegistry. + */ + before(async function () { + const [root] = await ethers.getSigners(); + + fakeAccessControlManager = await smock.fake("AccessControlManager"); + fakeAccessControlManager.isAllowedToCall.returns(true); + + const PoolRegistry = await ethers.getContractFactory("PoolRegistry"); + poolRegistry = (await upgrades.deployProxy(PoolRegistry, [fakeAccessControlManager.address])) as PoolRegistry; + + // Deploy Mock Tokens + const MockWBTC = await ethers.getContractFactory("MockToken"); + mockWBTC = await MockWBTC.deploy("Bitcoin", "BTC", 8); + + const _closeFactor = convertToUnit(0.05, 18); + const _liquidationIncentive = convertToUnit(1, 18); + const _minLiquidatableCollateral = convertToUnit(100, 18); + const maxLoopsLimit = 150; + + // Deploy Price Oracle + const MockPriceOracle = await ethers.getContractFactory("MockPriceOracle"); + const priceOracle = await MockPriceOracle.deploy(); + + const btcPrice = "21000.34"; + + await priceOracle.setPrice(mockWBTC.address, convertToUnit(btcPrice, 28)); + + const Comptroller = await ethers.getContractFactory("Comptroller"); + const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + + comptroller1Proxy = (await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ + maxLoopsLimit, + fakeAccessControlManager.address, + ])) as Comptroller; + await comptroller1Proxy.setPriceOracle(priceOracle.address); + + // Registering the first pool + await poolRegistry.addPool( + "Pool 1", + comptroller1Proxy.address, + _closeFactor, + _liquidationIncentive, + _minLiquidatableCollateral, + ); + + vTokenBeacon = await deployVTokenBeacon(); + const vWBTC = await makeVToken({ + underlying: mockWBTC.address, + comptroller: comptroller1Proxy.address, + accessControlManager: fakeAccessControlManager.address, + admin: root.address, + beacon: vTokenBeacon, + isTimeBased: isTimeBased, + blocksPerYear: slotsPerYear, + }); + + const initialSupply = parseUnits("1000", 18); + await mockWBTC.faucet(initialSupply); + await mockWBTC.approve(poolRegistry.address, initialSupply); + + // Deploy VTokens + await poolRegistry.addMarket({ + vToken: vWBTC.address, + collateralFactor: parseUnits("0.7", 18), + liquidationThreshold: parseUnits("0.7", 18), + initialSupply, + vTokenReceiver: root.address, + supplyCap: initialSupply, + borrowCap: initialSupply, + }); }); - const initialSupply = parseUnits("1000", 18); - await mockWBTC.faucet(initialSupply); - await mockWBTC.approve(poolRegistry.address, initialSupply); - - // Deploy VTokens - await poolRegistry.addMarket({ - vToken: vWBTC.address, - collateralFactor: parseUnits("0.7", 18), - liquidationThreshold: parseUnits("0.7", 18), - initialSupply, - vTokenReceiver: root.address, - supplyCap: initialSupply, - borrowCap: initialSupply, - }); - }); + it("Upgrade the vToken contract", async function () { + const maxBorrowRateMantissa = ethers.BigNumber.from(0.0005e16); - it("Upgrade the vToken contract", async function () { - const vToken = await ethers.getContractFactory("UpgradedVToken"); - const vTokenDeploy = await vToken.deploy(); - await vTokenDeploy.deployed(); + const vToken = await ethers.getContractFactory("UpgradedVToken"); + const vTokenDeploy = await vToken.deploy(isTimeBased, slotsPerYear, maxBorrowRateMantissa); - await vTokenBeacon.upgradeTo(vTokenDeploy.address); - const upgradeTo = await vTokenBeacon.callStatic.implementation(); - expect(upgradeTo).to.be.equal(vTokenDeploy.address); + await vTokenDeploy.deployed(); - const pools = await poolRegistry.callStatic.getAllPools(); - const vWBTCAddress = await poolRegistry.getVTokenForAsset(pools[0].comptroller, mockWBTC.address); - const vWBTC = await ethers.getContractAt("UpgradedVToken", vWBTCAddress); + await vTokenBeacon.upgradeTo(vTokenDeploy.address); + const upgradeTo = await vTokenBeacon.callStatic.implementation(); + expect(upgradeTo).to.be.equal(vTokenDeploy.address); + const pools = await poolRegistry.callStatic.getAllPools(); + const vWBTCAddress = await poolRegistry.getVTokenForAsset(pools[0].comptroller, mockWBTC.address); + const vWBTC = await ethers.getContractAt("UpgradedVToken", vWBTCAddress); - expect(2).to.be.equal(await vWBTC.version()); + if (!isTimeBased) { + const blockNumber = (await ethers.provider.getBlock("latest")).number; + expect(await vWBTC.getBlockNumberOrTimestamp()).to.closeTo(blockNumber, 10); + } else { + const blockTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + expect(await vWBTC.getBlockNumberOrTimestamp()).to.closeTo(blockTimestamp, 10); + } + + expect(2).to.be.equal(await vWBTC.version()); + }); }); -}); +} diff --git a/tests/hardhat/WhitePaperInterestRateModel.ts b/tests/hardhat/WhitePaperInterestRateModel.ts index 646bfc2ae..73333763e 100644 --- a/tests/hardhat/WhitePaperInterestRateModel.ts +++ b/tests/hardhat/WhitePaperInterestRateModel.ts @@ -4,86 +4,92 @@ import BigNumber from "bignumber.js"; import chai from "chai"; import { ethers } from "hardhat"; -import { blocksPerYear as blocksPerYearConfig } from "../../helpers/deploymentConfig"; +import { BSC_BLOCKS_PER_YEAR } from "../../helpers/deploymentConfig"; import { convertToUnit } from "../../helpers/utils"; import { WhitePaperInterestRateModel } from "../../typechain"; +import { getDescription } from "./util/descriptionHelpers"; const { expect } = chai; chai.use(smock.matchers); -describe("White paper interest rate model tests", () => { - let whitePaperInterestRateModel: WhitePaperInterestRateModel; +for (const isTimeBased of [false, true]) { + let interestRateModel: WhitePaperInterestRateModel; const cash = convertToUnit(10, 19); const borrows = convertToUnit(4, 19); const reserves = convertToUnit(2, 19); const badDebt = convertToUnit(1, 19); const expScale = convertToUnit(1, 18); - const blocksPerYear = blocksPerYearConfig.hardhat; const baseRatePerYear = convertToUnit(2, 12); const multiplierPerYear = convertToUnit(4, 14); + const description: string = getDescription(isTimeBased); + let slotsPerYear = isTimeBased ? 0 : BSC_BLOCKS_PER_YEAR; - const fixture = async () => { - const WhitePaperInterestRateModelFactory = await ethers.getContractFactory("WhitePaperInterestRateModel"); - whitePaperInterestRateModel = await WhitePaperInterestRateModelFactory.deploy( - blocksPerYear, - baseRatePerYear, - multiplierPerYear, - ); - await whitePaperInterestRateModel.deployed(); - }; + describe(`${description}White Paper interest rate model tests`, () => { + const fixture = async () => { + const interestRateModelFactory = await ethers.getContractFactory("WhitePaperInterestRateModel"); + interestRateModel = await interestRateModelFactory.deploy( + baseRatePerYear, + multiplierPerYear, + isTimeBased, + slotsPerYear, + ); + await interestRateModel.deployed(); + }; - before(async () => { - await loadFixture(fixture); - }); + before(async () => { + await loadFixture(fixture); + slotsPerYear = (await interestRateModel.blocksOrSecondsPerYear()).toNumber(); + }); - it("Model getters", async () => { - const baseRatePerBlock = new BigNumber(baseRatePerYear).dividedBy(blocksPerYear).toFixed(0); - const multiplierPerBlock = new BigNumber(multiplierPerYear).dividedBy(blocksPerYear).toFixed(0); + it("Model getters", async () => { + const baseRatePerBlockOrTimestamp = new BigNumber(baseRatePerYear).dividedBy(slotsPerYear).toFixed(0); + const multiplierPerBlockOrTimestamp = new BigNumber(multiplierPerYear).dividedBy(slotsPerYear).toFixed(0); - expect(await whitePaperInterestRateModel.blocksPerYear()).equal(blocksPerYear); - expect(await whitePaperInterestRateModel.baseRatePerBlock()).equal(baseRatePerBlock); - expect(await whitePaperInterestRateModel.multiplierPerBlock()).equal(multiplierPerBlock); - }); + expect(await interestRateModel.baseRatePerBlock()).equal(baseRatePerBlockOrTimestamp); + expect(await interestRateModel.multiplierPerBlock()).equal(multiplierPerBlockOrTimestamp); + }); - it("Utilization rate: borrows and badDebt is zero", async () => { - expect(await whitePaperInterestRateModel.utilizationRate(cash, 0, badDebt, 0)).equal(0); - }); + it("Utilization rate: borrows and badDebt is zero", async () => { + expect(await interestRateModel.utilizationRate(cash, 0, badDebt, 0)).equal(0); + }); - it("Utilization rate", async () => { - const utilizationRate = new BigNumber(Number(borrows) + Number(badDebt)) - .multipliedBy(expScale) - .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)) - .toFixed(0); + it("Utilization rate", async () => { + const utilizationRate = new BigNumber(Number(borrows) + Number(badDebt)) + .multipliedBy(expScale) + .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)) + .toFixed(0); - expect(await whitePaperInterestRateModel.utilizationRate(cash, borrows, reserves, badDebt)).equal(utilizationRate); - }); + expect(await interestRateModel.utilizationRate(cash, borrows, reserves, badDebt)).equal(utilizationRate); + }); - it("Borrow Rate", async () => { - const multiplierPerBlock = (await whitePaperInterestRateModel.multiplierPerBlock()).toString(); - const baseRatePerBlock = (await whitePaperInterestRateModel.baseRatePerBlock()).toString(); - const utilizationRate = ( - await whitePaperInterestRateModel.utilizationRate(cash, borrows, reserves, badDebt) - ).toString(); + it("Borrow Rate", async () => { + const multiplierPerBlockOrTimestamp = (await interestRateModel.multiplierPerBlock()).toString(); + const baseRatePerBlockOrTimestamp = (await interestRateModel.baseRatePerBlock()).toString(); + const utilizationRate = (await interestRateModel.utilizationRate(cash, borrows, reserves, badDebt)).toString(); - const value = new BigNumber(utilizationRate).multipliedBy(multiplierPerBlock).dividedBy(expScale).toFixed(0); + const value = new BigNumber(utilizationRate) + .multipliedBy(multiplierPerBlockOrTimestamp) + .dividedBy(expScale) + .toFixed(0); - expect(await whitePaperInterestRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).equal( - Number(value) + Number(baseRatePerBlock), - ); - }); + expect(await interestRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).equal( + Number(value) + Number(baseRatePerBlockOrTimestamp), + ); + }); - it("Supply Rate", async () => { - const reserveMantissa = convertToUnit(1, 17); - const oneMinusReserveFactor = Number(expScale) - Number(reserveMantissa); - const borrowRate = (await whitePaperInterestRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).toString(); - const rateToPool = new BigNumber(borrowRate).multipliedBy(oneMinusReserveFactor).dividedBy(expScale).toFixed(0); - const rate = new BigNumber(borrows) - .multipliedBy(expScale) - .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)); - const supplyRate = new BigNumber(rateToPool).multipliedBy(rate).dividedBy(expScale).toFixed(0); + it("Supply Rate", async () => { + const reserveMantissa = convertToUnit(1, 17); + const oneMinusReserveFactor = Number(expScale) - Number(reserveMantissa); + const borrowRate = (await interestRateModel.getBorrowRate(cash, borrows, reserves, badDebt)).toString(); + const rateToPool = new BigNumber(borrowRate).multipliedBy(oneMinusReserveFactor).dividedBy(expScale).toFixed(0); + const rate = new BigNumber(borrows) + .multipliedBy(expScale) + .dividedBy(Number(cash) + Number(borrows) + Number(badDebt) - Number(reserves)); + const supplyRate = new BigNumber(rateToPool).multipliedBy(rate).dividedBy(expScale).toFixed(0); - expect( - await whitePaperInterestRateModel.getSupplyRate(cash, borrows, reserves, convertToUnit(1, 17), badDebt), - ).equal(supplyRate); + expect(await interestRateModel.getSupplyRate(cash, borrows, reserves, convertToUnit(1, 17), badDebt)).equal( + supplyRate, + ); + }); }); -}); +} diff --git a/tests/hardhat/util/TokenTestHelpers.ts b/tests/hardhat/util/TokenTestHelpers.ts index 34c18270f..bbb3f22d7 100644 --- a/tests/hardhat/util/TokenTestHelpers.ts +++ b/tests/hardhat/util/TokenTestHelpers.ts @@ -4,6 +4,7 @@ import { BaseContract, BigNumber, BigNumberish, Signer } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { ethers, upgrades } from "hardhat"; +import { BSC_BLOCKS_PER_YEAR } from "../../../helpers/deploymentConfig"; import { AccessControlManager, Comptroller, @@ -34,6 +35,9 @@ interface VTokenParameters { protocolShareReserve: AddressOrContract; reserveFactorMantissa: BigNumberish; beacon: UpgradeableBeacon; + isTimeBased: boolean; + blocksPerYear: BigNumberish; + maxBorrowRateMantissa: BigNumberish; } const getNameAndSymbol = async (underlying: AddressOrContract): Promise<[string, string]> => { @@ -71,9 +75,14 @@ export type AnyVTokenFactory = VTokenHarness__factory | VToken__factory; export const deployVTokenBeacon = async ( { kind }: { kind: string } = { kind: "VToken" }, + maxBorrowRateMantissa: BigNumberish = BigNumber.from(0.0005e16), + isTimeBased: boolean = false, + blocksPerYear: BigNumberish = BSC_BLOCKS_PER_YEAR, ): Promise => { const VToken = await ethers.getContractFactory(kind); - const vTokenBeacon = (await upgrades.deployBeacon(VToken)) as UpgradeableBeacon; + const vTokenBeacon = (await upgrades.deployBeacon(VToken, { + constructorArgs: [isTimeBased, blocksPerYear, maxBorrowRateMantissa], + })) as UpgradeableBeacon; return vTokenBeacon; }; @@ -99,7 +108,17 @@ const deployVTokenDependencies = async ({ kind })), + maxBorrowRateMantissa: params.maxBorrowRateMantissa || BigNumber.from(0.0005e16), + beacon: + params.beacon || + (await deployVTokenBeacon( + { kind }, + params.maxBorrowRateMantissa || BigNumber.from(0.0005e16), + params.isTimeBased || false, + params.blocksPerYear === undefined ? BSC_BLOCKS_PER_YEAR : params.blocksPerYear, + )), + isTimeBased: params.isTimeBased || false, + blocksPerYear: params.blocksPerYear === undefined ? BSC_BLOCKS_PER_YEAR : params.blocksPerYear, }; }; @@ -108,6 +127,7 @@ export const makeVToken = async > => { const params_ = await deployVTokenDependencies(params, { kind }); + const VToken = await ethers.getContractFactory(kind); const vToken = (await upgrades.deployBeaconProxy(params_.beacon, VToken, [ @@ -126,6 +146,7 @@ export const makeVToken = async ; + return vToken; }; diff --git a/tests/hardhat/util/descriptionHelpers.ts b/tests/hardhat/util/descriptionHelpers.ts new file mode 100644 index 000000000..5b0c61998 --- /dev/null +++ b/tests/hardhat/util/descriptionHelpers.ts @@ -0,0 +1,6 @@ +export function getDescription(isTimeBased: boolean) { + if (!isTimeBased) { + return ""; + } + return "TimeBased "; +} diff --git a/tests/integration/index.ts b/tests/integration/index.ts index dd64d2a03..f504eb70e 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -23,6 +23,14 @@ import { Error } from "../hardhat/util/Errors"; const { expect } = chai; chai.use(smock.matchers); +const timeBasedIntegrationTests = process.env.IS_TIME_BASED_DEPLOYMENT === "true"; +let description: string = "block-based contracts"; + +if (timeBasedIntegrationTests) { + description = "time-based contracts"; +} +console.log(`integration tests are running for ${description}`); + const toggleMining = async (status: boolean) => { if (!status) { await network.provider.send("evm_setAutomine", [false]); @@ -31,7 +39,7 @@ const toggleMining = async (status: boolean) => { } }; -const setupTest = deployments.createFixture(async ({ deployments, getNamedAccounts, ethers }: any) => { +const setupTest = deployments.createFixture(async ({ deployments, getNamedAccounts, ethers }) => { await deployments.fixture(); const { deployer, acc1, acc2, acc3 } = await getNamedAccounts(); const PoolRegistry: PoolRegistry = await ethers.getContract("PoolRegistry"); @@ -377,7 +385,11 @@ describe("Positive Cases", function () { await vBTCB.connect(acc2Signer).borrow(BTCBBorrowAmount); // Mining blocks - await mine(300000000); + if (timeBasedIntegrationTests) { + await mine(700000000); + } else { + await mine(300000000); + } await BTCB.connect(acc1Signer).approve(vBTCB.address, convertToUnit(10, 18)); await Comptroller.connect(acc1Signer).healAccount(acc2); @@ -497,7 +509,9 @@ describe("Straight Cases For Single User Liquidation and healing", function () { ); await Comptroller.setPriceOracle(dummyPriceOracle.address); - const repayAmount = convertToUnit("1000000000007133", 0); + + let repayAmount = 1000000000007133; + if (timeBasedIntegrationTests) repayAmount = 1000000000002377; const param = { vTokenCollateral: vBNX.address, vTokenBorrowed: vBTCB.address, diff --git a/yarn.lock b/yarn.lock index cccee9046..f5c807b43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3171,6 +3171,7 @@ __metadata: "@venusprotocol/governance-contracts": 2.0.0 "@venusprotocol/oracle": 2.0.0 "@venusprotocol/protocol-reserve": 2.0.0 + "@venusprotocol/solidity-utilities": ^2.0.0 "@venusprotocol/venus-protocol": 8.0.0 bignumber.js: 9.0.0 chai: ^4.3.6