diff --git a/.circleci/config.yml b/.circleci/config.yml index cfc346165..89a3dc82d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,6 +115,7 @@ jobs: - run: shell: /bin/bash -eox pipefail -O globstar name: yarn test + no_output_timeout: 30m command: JEST_JUNIT_OUTPUT_DIR=~/junit JEST_JUNIT_OUTPUT_NAME=test-results.xml script/coverage $(circleci tests glob 'tests/**/**Test.js' | circleci tests split --split-by=timings) -- --maxWorkers=4 - save_cache: paths: diff --git a/.soliumrc.json b/.soliumrc.json index c3600c074..fdf4be8b4 100644 --- a/.soliumrc.json +++ b/.soliumrc.json @@ -15,6 +15,8 @@ ], "security/no-block-members": "off", "security/no-inline-assembly": "off", - "security/no-low-level-calls": "off" + "security/no-low-level-calls": "off", + "security/no-tx-origin": "off", + "imports-on-top": "off" } } diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 72dd15fa3..05b93a777 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -7,67 +7,61 @@ import "./PriceOracle.sol"; import "./ComptrollerInterface.sol"; import "./ComptrollerStorage.sol"; import "./Unitroller.sol"; +import "./Governance/Comp.sol"; /** * @title Compound's Comptroller Contract * @author Compound */ -contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { - /** - * @notice Emitted when an admin supports a market - */ +contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { + /// @notice Emitted when an admin supports a market event MarketListed(CToken cToken); - /** - * @notice Emitted when an account enters a market - */ + /// @notice Emitted when an account enters a market event MarketEntered(CToken cToken, address account); - /** - * @notice Emitted when an account exits a market - */ + /// @notice Emitted when an account exits a market event MarketExited(CToken cToken, address account); - /** - * @notice Emitted when close factor is changed by admin - */ + /// @notice Emitted when close factor is changed by admin event NewCloseFactor(uint oldCloseFactorMantissa, uint newCloseFactorMantissa); - /** - * @notice Emitted when a collateral factor is changed by admin - */ + /// @notice Emitted when a collateral factor is changed by admin event NewCollateralFactor(CToken cToken, uint oldCollateralFactorMantissa, uint newCollateralFactorMantissa); - /** - * @notice Emitted when liquidation incentive is changed by admin - */ + /// @notice Emitted when liquidation incentive is changed by admin event NewLiquidationIncentive(uint oldLiquidationIncentiveMantissa, uint newLiquidationIncentiveMantissa); - /** - * @notice Emitted when maxAssets is changed by admin - */ + /// @notice Emitted when maxAssets is changed by admin event NewMaxAssets(uint oldMaxAssets, uint newMaxAssets); - /** - * @notice Emitted when price oracle is changed - */ + /// @notice Emitted when price oracle is changed event NewPriceOracle(PriceOracle oldPriceOracle, PriceOracle newPriceOracle); - /** - * @notice Emitted when pause guardian is changed - */ + /// @notice Emitted when pause guardian is changed event NewPauseGuardian(address oldPauseGuardian, address newPauseGuardian); - /** - * @notice Emitted when an action is paused globally - */ + /// @notice Emitted when an action is paused globally event ActionPaused(string action, bool pauseState); - /** - * @notice Emitted when an action is paused on a market - */ + /// @notice Emitted when an action is paused on a market event ActionPaused(CToken cToken, string action, bool pauseState); + /// @notice Emitted when a new COMP speed is calculated for a market + event CompSpeedUpdated(CToken indexed cToken, uint newSpeed); + + /// @notice Emitted when COMP is distributed to a supplier + event DistributedSupplierComp(CToken indexed cToken, address indexed supplier, uint compDelta, uint compSupplyIndex); + + /// @notice Emitted when COMP is distributed to a borrower + event DistributedBorrowerComp(CToken indexed cToken, address indexed borrower, uint compDelta, uint compBorrowIndex); + + /// @notice The threshold at which the flywheel starts to distribute COMP, in wei + uint public constant compClaimThreshold = 0.01e18; + + /// @notice The initial COMP index for a market + uint224 public constant compInitialIndex = 1e36; + // closeFactorMantissa must be strictly greater than this value uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05 @@ -245,7 +239,9 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.MARKET_NOT_LISTED); } - // *may include Policy Hook-type checks + // Keep the flywheel moving + updateCompSupplyIndex(cToken); + distributeSupplierComp(cToken, minter); return uint(Error.NO_ERROR); } @@ -278,7 +274,16 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE * @return 0 if the redeem is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) */ function redeemAllowed(address cToken, address redeemer, uint redeemTokens) external returns (uint) { - return redeemAllowedInternal(cToken, redeemer, redeemTokens); + uint allowed = redeemAllowedInternal(cToken, redeemer, redeemTokens); + if (allowed != uint(Error.NO_ERROR)) { + return allowed; + } + + // Keep the flywheel moving + updateCompSupplyIndex(cToken); + distributeSupplierComp(cToken, redeemer); + + return uint(Error.NO_ERROR); } function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) { @@ -286,8 +291,6 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.MARKET_NOT_LISTED); } - // *may include Policy Hook-type checks - /* If the redeemer is not 'in' the market, then we can bypass the liquidity check */ if (!markets[cToken].accountMembership[redeemer]) { return uint(Error.NO_ERROR); @@ -338,8 +341,6 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.MARKET_NOT_LISTED); } - // *may include Policy Hook-type checks - if (!markets[cToken].accountMembership[borrower]) { // only cTokens may call borrowAllowed if borrower not in market require(msg.sender == cToken, "sender must be cToken"); @@ -366,6 +367,11 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.INSUFFICIENT_LIQUIDITY); } + // Keep the flywheel moving + Exp memory borrowIndex = Exp({mantissa: CToken(cToken).borrowIndex()}); + updateCompBorrowIndex(cToken, borrowIndex); + distributeBorrowerComp(cToken, borrower, borrowIndex); + return uint(Error.NO_ERROR); } @@ -409,7 +415,10 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.MARKET_NOT_LISTED); } - // *may include Policy Hook-type checks + // Keep the flywheel moving + Exp memory borrowIndex = Exp({mantissa: CToken(cToken).borrowIndex()}); + updateCompBorrowIndex(cToken, borrowIndex); + distributeBorrowerComp(cToken, borrower, borrowIndex); return uint(Error.NO_ERROR); } @@ -461,8 +470,6 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.MARKET_NOT_LISTED); } - // *may include Policy Hook-type checks - /* The borrower must have shortfall in order to be liquidatable */ (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower); if (err != Error.NO_ERROR) { @@ -532,8 +539,6 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE require(!seizeGuardianPaused, "seize is paused"); // Shh - currently unused - liquidator; - borrower; seizeTokens; if (!markets[cTokenCollateral].isListed || !markets[cTokenBorrowed].isListed) { @@ -544,7 +549,10 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return uint(Error.COMPTROLLER_MISMATCH); } - // *may include Policy Hook-type checks + // Keep the flywheel moving + updateCompSupplyIndex(cTokenCollateral); + distributeSupplierComp(cTokenCollateral, borrower); + distributeSupplierComp(cTokenCollateral, liquidator); return uint(Error.NO_ERROR); } @@ -588,14 +596,19 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE // Pausing is a very serious situation - we revert to sound the alarms require(!transferGuardianPaused, "transfer is paused"); - // Shh - currently unused - dst; - - // *may include Policy Hook-type checks - // Currently the only consideration is whether or not // the src is allowed to redeem this many tokens - return redeemAllowedInternal(cToken, src, transferTokens); + uint allowed = redeemAllowedInternal(cToken, src, transferTokens); + if (allowed != uint(Error.NO_ERROR)) { + return allowed; + } + + // Keep the flywheel moving + updateCompSupplyIndex(cToken); + distributeSupplierComp(cToken, src); + distributeSupplierComp(cToken, dst); + + return uint(Error.NO_ERROR); } /** @@ -635,7 +648,7 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE Exp collateralFactor; Exp exchangeRate; Exp oraclePrice; - Exp tokensToEther; + Exp tokensToDenom; } /** @@ -722,13 +735,13 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa}); // Pre-compute a conversion factor from tokens -> ether (normalized price value) - (mErr, vars.tokensToEther) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice); + (mErr, vars.tokensToDenom) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice); if (mErr != MathError.NO_ERROR) { return (Error.MATH_ERROR, 0, 0); } - // sumCollateral += tokensToEther * cTokenBalance - (mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToEther, vars.cTokenBalance, vars.sumCollateral); + // sumCollateral += tokensToDenom * cTokenBalance + (mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral); if (mErr != MathError.NO_ERROR) { return (Error.MATH_ERROR, 0, 0); } @@ -742,8 +755,8 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE // Calculate effects of interacting with cTokenModify if (asset == cTokenModify) { // redeem effect - // sumBorrowPlusEffects += tokensToEther * redeemTokens - (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToEther, redeemTokens, vars.sumBorrowPlusEffects); + // sumBorrowPlusEffects += tokensToDenom * redeemTokens + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects); if (mErr != MathError.NO_ERROR) { return (Error.MATH_ERROR, 0, 0); } @@ -848,7 +861,7 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE * @param newCloseFactorMantissa New close factor, scaled by 1e18 * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) */ - function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint256) { + function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint) { // Check caller is admin if (msg.sender != admin) { return fail(Error.UNAUTHORIZED, FailureInfo.SET_CLOSE_FACTOR_OWNER_CHECK); @@ -879,7 +892,7 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE * @param newCollateralFactorMantissa The new collateral factor, scaled by 1e18 * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) */ - function _setCollateralFactor(CToken cToken, uint newCollateralFactorMantissa) external returns (uint256) { + function _setCollateralFactor(CToken cToken, uint newCollateralFactorMantissa) external returns (uint) { // Check caller is admin if (msg.sender != admin) { return fail(Error.UNAUTHORIZED, FailureInfo.SET_COLLATERAL_FACTOR_OWNER_CHECK); @@ -1052,10 +1065,268 @@ contract Comptroller is ComptrollerV2Storage, ComptrollerInterface, ComptrollerE return state; } - function _become(Unitroller unitroller) public { + function _become(Unitroller unitroller, uint compRate_, address[] memory compMarkets_) public { require(msg.sender == unitroller.admin(), "only unitroller admin can change brains"); + require(unitroller._acceptImplementation() == 0, "change not authorized"); + + Comptroller freshBrainedComptroller = Comptroller(address(unitroller)); + freshBrainedComptroller._setCompRate(compRate_); + freshBrainedComptroller._addCompMarkets(compMarkets_); + } + + /** + * @notice Checks caller is admin, or this contract is becoming the new implementation + */ + function adminOrInitializing() internal view returns (bool) { + return msg.sender == admin || msg.sender == comptrollerImplementation; + } - uint changeStatus = unitroller._acceptImplementation(); - require(changeStatus == 0, "change not authorized"); + /*** Comp Flywheel Distribution ***/ + + /** + * @notice Recalculate and update COMP speeds for all COMP markets + */ + function refreshCompSpeeds() public { + CToken[] memory markets = compMarkets; + + for (uint i = 0; i < markets.length; i++) { + CToken cToken = markets[i]; + Exp memory borrowIndex = Exp({mantissa: cToken.borrowIndex()}); + updateCompSupplyIndex(address(cToken)); + updateCompBorrowIndex(address(cToken), borrowIndex); + } + + Exp memory totalUtility = Exp({mantissa: 0}); + Exp[] memory utilities = new Exp[](markets.length); + for (uint i = 0; i < markets.length; i++) { + CToken cToken = markets[i]; + Exp memory assetPrice = Exp({mantissa: oracle.getUnderlyingPrice(cToken)}); + Exp memory interestPerBlock = mul_(Exp({mantissa: cToken.borrowRatePerBlock()}), cToken.totalBorrows()); + Exp memory utility = mul_(interestPerBlock, assetPrice); + utilities[i] = utility; + totalUtility = add_(totalUtility, utility); + } + + if (totalUtility.mantissa > 0) { + for (uint i = 0; i < markets.length; i++) { + CToken cToken = markets[i]; + uint newSpeed = mul_(compRate, div_(utilities[i], totalUtility)); + compSpeeds[address(cToken)] = newSpeed; + emit CompSpeedUpdated(cToken, newSpeed); + } + } + } + + /** + * @notice Accrue COMP to the market by updating the supply index + * @param cToken The market whose supply index to update + */ + function updateCompSupplyIndex(address cToken) internal { + CompMarketState storage supplyState = compSupplyState[cToken]; + uint supplySpeed = compSpeeds[cToken]; + uint blockNumber = getBlockNumber(); + uint deltaBlocks = sub_(blockNumber, uint(supplyState.block)); + if (deltaBlocks > 0 && supplySpeed > 0) { + uint supplyTokens = CToken(cToken).totalSupply(); + uint compAccrued = mul_(deltaBlocks, supplySpeed); + Double memory index = add_(Double({mantissa: supplyState.index}), fraction(compAccrued, supplyTokens)); + supplyState.index = safe224(index.mantissa, "new index exceeds 224 bits"); + supplyState.block = safe32(blockNumber, "block number exceeds 32 bits"); + } else if (deltaBlocks > 0) { + supplyState.block = safe32(blockNumber, "block number exceeds 32 bits"); + } + } + + /** + * @notice Accrue COMP to the market by updating the borrow index + * @param cToken The market whose borrow index to update + */ + function updateCompBorrowIndex(address cToken, Exp memory marketBorrowIndex) internal { + CompMarketState storage borrowState = compBorrowState[cToken]; + uint borrowSpeed = compSpeeds[cToken]; + uint blockNumber = getBlockNumber(); + uint deltaBlocks = sub_(blockNumber, uint(borrowState.block)); + if (deltaBlocks > 0 && borrowSpeed > 0) { + uint borrowAmount = div_(CToken(cToken).totalBorrows(), marketBorrowIndex); + uint compAccrued = mul_(deltaBlocks, borrowSpeed); + Double memory index = add_(Double({mantissa: borrowState.index}), fraction(compAccrued, borrowAmount)); + borrowState.index = safe224(index.mantissa, "new index exceeds 224 bits"); + borrowState.block = safe32(blockNumber, "block number exceeds 32 bits"); + } else if (deltaBlocks > 0) { + borrowState.block = safe32(blockNumber, "block number exceeds 32 bits"); + } + } + + /** + * @notice Calculate COMP accrued by a supplier and possibly transfer it to them + * @param cToken The market in which the supplier is interacting + * @param supplier The address of the supplier to distribute COMP to + */ + function distributeSupplierComp(address cToken, address supplier) internal { + CompMarketState storage supplyState = compSupplyState[cToken]; + Double memory supplyIndex = Double({mantissa: supplyState.index}); + + /* Short-circuit if this is not a COMP market */ + if (supplyIndex.mantissa == 0) { + return; + } + + Double memory supplierIndex = Double({mantissa: compSupplierIndex[cToken][supplier]}); + if (supplierIndex.mantissa == 0) { + supplierIndex.mantissa = compInitialIndex; + } + Double memory deltaIndex = sub_(supplyIndex, supplierIndex); + uint supplierTokens = CToken(cToken).balanceOf(supplier); + uint supplierDelta = mul_(supplierTokens, deltaIndex); + uint supplierAccrued = add_(compAccrued[supplier], supplierDelta); + compSupplierIndex[cToken][supplier] = supplyIndex.mantissa; + compAccrued[supplier] = transferComp(supplier, supplierAccrued, compClaimThreshold); + emit DistributedSupplierComp(CToken(cToken), supplier, supplierDelta, supplyIndex.mantissa); + } + + /** + * @notice Calculate COMP accrued by a borrower and possibly transfer it to them + * @dev Borrowers will not begin to accrue until after the first interaction with the protocol. + * @param cToken The market in which the borrower is interacting + * @param borrower The address of the borrower to distribute COMP to + */ + function distributeBorrowerComp(address cToken, address borrower, Exp memory marketBorrowIndex) internal { + CompMarketState storage borrowState = compBorrowState[cToken]; + Double memory borrowIndex = Double({mantissa: borrowState.index}); + + /* Short-circuit if this is not a COMP market */ + if (borrowIndex.mantissa == 0) { + return; + } + + Double memory borrowerIndex = Double({mantissa: compBorrowerIndex[cToken][borrower]}); + compBorrowerIndex[cToken][borrower] = borrowIndex.mantissa; + + if (borrowerIndex.mantissa > 0) { + Double memory deltaIndex = sub_(borrowIndex, borrowerIndex); + uint borrowerAmount = div_(CToken(cToken).borrowBalanceStored(borrower), marketBorrowIndex); + uint borrowerDelta = mul_(borrowerAmount, deltaIndex); + uint borrowerAccrued = add_(compAccrued[borrower], borrowerDelta); + compAccrued[borrower] = transferComp(borrower, borrowerAccrued, compClaimThreshold); + emit DistributedBorrowerComp(CToken(cToken), borrower, borrowerDelta, borrowIndex.mantissa); + } + } + + /** + * @notice Transfer COMP to the user, if they are above the threshold + * @dev Note: If there is not enough COMP, we do not perform the transfer all. + * @param user The address of the user to transfer COMP to + * @param userAccrued The amount of COMP to (possibly) transfer + * @return The amount of COMP which was NOT transferred to the user + */ + function transferComp(address user, uint userAccrued, uint threshold) internal returns (uint) { + if (userAccrued >= threshold) { + Comp comp = Comp(getCompAddress()); + uint compRemaining = comp.balanceOf(address(this)); + if (userAccrued <= compRemaining) { + comp.transfer(user, userAccrued); + return 0; + } + } + return userAccrued; + } + + /** + * @notice Claim all the comp accrued by holder + * @param holder The address to claim comp for + */ + function claimComp(address holder) public { + refreshCompSpeeds(); + + CToken[] memory markets = compMarkets; + for (uint i = 0; i < markets.length; i++) { + CToken cToken = markets[i]; + Exp memory borrowIndex = Exp({mantissa: cToken.borrowIndex()}); + distributeSupplierComp(address(cToken), holder); + distributeBorrowerComp(address(cToken), holder, borrowIndex); + } + + compAccrued[holder] = transferComp(holder, compAccrued[holder], 0); + } + + /*** Comp Flyhwheel Admin ***/ + + /** + * @notice Set the amount of COMP distributed per block + * @param compRate_ The amount of COMP wei per block to distribute + */ + function _setCompRate(uint compRate_) public { + require(adminOrInitializing(), "only admin can change comp rate"); + compRate = compRate_; + refreshCompSpeeds(); + } + + /** + * @notice Add markets to compMarkets, allowing them to earn COMP in the flywheel + * @param cTokens The addresses of the markets to add + */ + function _addCompMarkets(address[] memory cTokens) public { + require(adminOrInitializing(), "only admin can add comp market"); + for (uint i = 0; i < cTokens.length; i++) { + _addCompMarketInternal(cTokens[i]); + } + refreshCompSpeeds(); + } + + function _addCompMarketInternal(address cToken) internal { + require(markets[cToken].isListed == true, "comp market is not listed"); + + for (uint i = 0; i < compMarkets.length; i ++) { + require(compMarkets[i] != CToken(cToken), "comp market already added"); + } + + compMarkets.push(CToken(cToken)); + + if (compSupplyState[cToken].index == 0 && compSupplyState[cToken].block == 0) { + compSupplyState[cToken].index = compInitialIndex; + compSupplyState[cToken].block = safe32(getBlockNumber(), "block number exceeds 32 bits"); + } + + if (compBorrowState[cToken].index == 0 && compBorrowState[cToken].block == 0) { + compBorrowState[cToken].index = compInitialIndex; + compBorrowState[cToken].block = safe32(getBlockNumber(), "block number exceeds 32 bits"); + } + } + + /** + * @notice Remove a market from compMarkets, preventing it from earning COMP in the flywheel + * @param cToken The address of the market to drop + */ + function _dropCompMarket(address cToken) public { + require(msg.sender == admin, "only admin can drop comp market"); + for (uint i = 0; i < compMarkets.length; i ++) { + if (compMarkets[i] == CToken(cToken)) { + compMarkets[i] = compMarkets[compMarkets.length - 1]; + compMarkets.length--; + break; + } + } + refreshCompSpeeds(); + } + + function getBlockNumber() public view returns (uint) { + return block.number; + } + + /** + * @notice Return the address of the COMP token + * @return The address of COMP + */ + function getCompAddress() public view returns (address) { + return 0xc00e94Cb662C3520282E6f5717214004A7f26888; + } + + /** + * @notice Return all of the COMP markets + * @dev The automatic getter may be used to access an individual market. + * @return The list of COMP markets + */ + function getCompMarkets() public view returns (CToken[] memory markets) { + return compMarkets; } } diff --git a/contracts/ComptrollerG2.sol b/contracts/ComptrollerG2.sol new file mode 100644 index 000000000..108183fdd --- /dev/null +++ b/contracts/ComptrollerG2.sol @@ -0,0 +1,1061 @@ +pragma solidity ^0.5.16; + +import "./CToken.sol"; +import "./ErrorReporter.sol"; +import "./Exponential.sol"; +import "./PriceOracle.sol"; +import "./ComptrollerInterface.sol"; +import "./ComptrollerStorage.sol"; +import "./Unitroller.sol"; + +/** + * @title Compound's Comptroller Contract + * @author Compound + */ +contract ComptrollerG2 is ComptrollerV2Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { + /** + * @notice Emitted when an admin supports a market + */ + event MarketListed(CToken cToken); + + /** + * @notice Emitted when an account enters a market + */ + event MarketEntered(CToken cToken, address account); + + /** + * @notice Emitted when an account exits a market + */ + event MarketExited(CToken cToken, address account); + + /** + * @notice Emitted when close factor is changed by admin + */ + event NewCloseFactor(uint oldCloseFactorMantissa, uint newCloseFactorMantissa); + + /** + * @notice Emitted when a collateral factor is changed by admin + */ + event NewCollateralFactor(CToken cToken, uint oldCollateralFactorMantissa, uint newCollateralFactorMantissa); + + /** + * @notice Emitted when liquidation incentive is changed by admin + */ + event NewLiquidationIncentive(uint oldLiquidationIncentiveMantissa, uint newLiquidationIncentiveMantissa); + + /** + * @notice Emitted when maxAssets is changed by admin + */ + event NewMaxAssets(uint oldMaxAssets, uint newMaxAssets); + + /** + * @notice Emitted when price oracle is changed + */ + event NewPriceOracle(PriceOracle oldPriceOracle, PriceOracle newPriceOracle); + + /** + * @notice Emitted when pause guardian is changed + */ + event NewPauseGuardian(address oldPauseGuardian, address newPauseGuardian); + + /** + * @notice Emitted when an action is paused globally + */ + event ActionPaused(string action, bool pauseState); + + /** + * @notice Emitted when an action is paused on a market + */ + event ActionPaused(CToken cToken, string action, bool pauseState); + + // closeFactorMantissa must be strictly greater than this value + uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05 + + // closeFactorMantissa must not exceed this value + uint internal constant closeFactorMaxMantissa = 0.9e18; // 0.9 + + // No collateralFactorMantissa may exceed this value + uint internal constant collateralFactorMaxMantissa = 0.9e18; // 0.9 + + // liquidationIncentiveMantissa must be no less than this value + uint internal constant liquidationIncentiveMinMantissa = 1.0e18; // 1.0 + + // liquidationIncentiveMantissa must be no greater than this value + uint internal constant liquidationIncentiveMaxMantissa = 1.5e18; // 1.5 + + constructor() public { + admin = msg.sender; + } + + /*** Assets You Are In ***/ + + /** + * @notice Returns the assets an account has entered + * @param account The address of the account to pull assets for + * @return A dynamic list with the assets the account has entered + */ + function getAssetsIn(address account) external view returns (CToken[] memory) { + CToken[] memory assetsIn = accountAssets[account]; + + return assetsIn; + } + + /** + * @notice Returns whether the given account is entered in the given asset + * @param account The address of the account to check + * @param cToken The cToken to check + * @return True if the account is in the asset, otherwise false. + */ + function checkMembership(address account, CToken cToken) external view returns (bool) { + return markets[address(cToken)].accountMembership[account]; + } + + /** + * @notice Add assets to be included in account liquidity calculation + * @param cTokens The list of addresses of the cToken markets to be enabled + * @return Success indicator for whether each corresponding market was entered + */ + function enterMarkets(address[] memory cTokens) public returns (uint[] memory) { + uint len = cTokens.length; + + uint[] memory results = new uint[](len); + for (uint i = 0; i < len; i++) { + CToken cToken = CToken(cTokens[i]); + + results[i] = uint(addToMarketInternal(cToken, msg.sender)); + } + + return results; + } + + /** + * @notice Add the market to the borrower's "assets in" for liquidity calculations + * @param cToken The market to enter + * @param borrower The address of the account to modify + * @return Success indicator for whether the market was entered + */ + function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) { + Market storage marketToJoin = markets[address(cToken)]; + + if (!marketToJoin.isListed) { + // market is not listed, cannot join + return Error.MARKET_NOT_LISTED; + } + + if (marketToJoin.accountMembership[borrower] == true) { + // already joined + return Error.NO_ERROR; + } + + if (accountAssets[borrower].length >= maxAssets) { + // no space, cannot join + return Error.TOO_MANY_ASSETS; + } + + // survived the gauntlet, add to list + // NOTE: we store these somewhat redundantly as a significant optimization + // this avoids having to iterate through the list for the most common use cases + // that is, only when we need to perform liquidity checks + // and not whenever we want to check if an account is in a particular market + marketToJoin.accountMembership[borrower] = true; + accountAssets[borrower].push(cToken); + + emit MarketEntered(cToken, borrower); + + return Error.NO_ERROR; + } + + /** + * @notice Removes asset from sender's account liquidity calculation + * @dev Sender must not have an outstanding borrow balance in the asset, + * or be providing neccessary collateral for an outstanding borrow. + * @param cTokenAddress The address of the asset to be removed + * @return Whether or not the account successfully exited the market + */ + function exitMarket(address cTokenAddress) external returns (uint) { + CToken cToken = CToken(cTokenAddress); + /* Get sender tokensHeld and amountOwed underlying from the cToken */ + (uint oErr, uint tokensHeld, uint amountOwed, ) = cToken.getAccountSnapshot(msg.sender); + require(oErr == 0, "exitMarket: getAccountSnapshot failed"); // semi-opaque error code + + /* Fail if the sender has a borrow balance */ + if (amountOwed != 0) { + return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED); + } + + /* Fail if the sender is not permitted to redeem all of their tokens */ + uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld); + if (allowed != 0) { + return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed); + } + + Market storage marketToExit = markets[address(cToken)]; + + /* Return true if the sender is not already ‘in’ the market */ + if (!marketToExit.accountMembership[msg.sender]) { + return uint(Error.NO_ERROR); + } + + /* Set cToken account membership to false */ + delete marketToExit.accountMembership[msg.sender]; + + /* Delete cToken from the account’s list of assets */ + // load into memory for faster iteration + CToken[] memory userAssetList = accountAssets[msg.sender]; + uint len = userAssetList.length; + uint assetIndex = len; + for (uint i = 0; i < len; i++) { + if (userAssetList[i] == cToken) { + assetIndex = i; + break; + } + } + + // We *must* have found the asset in the list or our redundant data structure is broken + assert(assetIndex < len); + + // copy last item in list to location of item to be removed, reduce length by 1 + CToken[] storage storedList = accountAssets[msg.sender]; + storedList[assetIndex] = storedList[storedList.length - 1]; + storedList.length--; + + emit MarketExited(cToken, msg.sender); + + return uint(Error.NO_ERROR); + } + + /*** Policy Hooks ***/ + + /** + * @notice Checks if the account should be allowed to mint tokens in the given market + * @param cToken The market to verify the mint against + * @param minter The account which would get the minted tokens + * @param mintAmount The amount of underlying being supplied to the market in exchange for tokens + * @return 0 if the mint is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function mintAllowed(address cToken, address minter, uint mintAmount) external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!mintGuardianPaused[cToken], "mint is paused"); + + // Shh - currently unused + minter; + mintAmount; + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates mint and reverts on rejection. May emit logs. + * @param cToken Asset being minted + * @param minter The address minting the tokens + * @param actualMintAmount The amount of the underlying asset being minted + * @param mintTokens The number of tokens being minted + */ + function mintVerify(address cToken, address minter, uint actualMintAmount, uint mintTokens) external { + // Shh - currently unused + cToken; + minter; + actualMintAmount; + mintTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to redeem tokens in the given market + * @param cToken The market to verify the redeem against + * @param redeemer The account which would redeem the tokens + * @param redeemTokens The number of cTokens to exchange for the underlying asset in the market + * @return 0 if the redeem is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function redeemAllowed(address cToken, address redeemer, uint redeemTokens) external returns (uint) { + return redeemAllowedInternal(cToken, redeemer, redeemTokens); + } + + function redeemAllowedInternal(address cToken, address redeemer, uint redeemTokens) internal view returns (uint) { + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + /* If the redeemer is not 'in' the market, then we can bypass the liquidity check */ + if (!markets[cToken].accountMembership[redeemer]) { + return uint(Error.NO_ERROR); + } + + /* Otherwise, perform a hypothetical liquidity check to guard against shortfall */ + (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(redeemer, CToken(cToken), redeemTokens, 0); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall > 0) { + return uint(Error.INSUFFICIENT_LIQUIDITY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates redeem and reverts on rejection. May emit logs. + * @param cToken Asset being redeemed + * @param redeemer The address redeeming the tokens + * @param redeemAmount The amount of the underlying asset being redeemed + * @param redeemTokens The number of tokens being redeemed + */ + function redeemVerify(address cToken, address redeemer, uint redeemAmount, uint redeemTokens) external { + // Shh - currently unused + cToken; + redeemer; + + // Require tokens is zero or amount is also zero + if (redeemTokens == 0 && redeemAmount > 0) { + revert("redeemTokens zero"); + } + } + + /** + * @notice Checks if the account should be allowed to borrow the underlying asset of the given market + * @param cToken The market to verify the borrow against + * @param borrower The account which would borrow the asset + * @param borrowAmount The amount of underlying the account would borrow + * @return 0 if the borrow is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function borrowAllowed(address cToken, address borrower, uint borrowAmount) external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!borrowGuardianPaused[cToken], "borrow is paused"); + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + if (!markets[cToken].accountMembership[borrower]) { + // only cTokens may call borrowAllowed if borrower not in market + require(msg.sender == cToken, "sender must be cToken"); + + // attempt to add borrower to the market + Error err = addToMarketInternal(CToken(msg.sender), borrower); + if (err != Error.NO_ERROR) { + return uint(err); + } + + // it should be impossible to break the important invariant + assert(markets[cToken].accountMembership[borrower]); + } + + if (oracle.getUnderlyingPrice(CToken(cToken)) == 0) { + return uint(Error.PRICE_ERROR); + } + + (Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(borrower, CToken(cToken), 0, borrowAmount); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall > 0) { + return uint(Error.INSUFFICIENT_LIQUIDITY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates borrow and reverts on rejection. May emit logs. + * @param cToken Asset whose underlying is being borrowed + * @param borrower The address borrowing the underlying + * @param borrowAmount The amount of the underlying asset requested to borrow + */ + function borrowVerify(address cToken, address borrower, uint borrowAmount) external { + // Shh - currently unused + cToken; + borrower; + borrowAmount; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to repay a borrow in the given market + * @param cToken The market to verify the repay against + * @param payer The account which would repay the asset + * @param borrower The account which would borrowed the asset + * @param repayAmount The amount of the underlying asset the account would repay + * @return 0 if the repay is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function repayBorrowAllowed( + address cToken, + address payer, + address borrower, + uint repayAmount) external returns (uint) { + // Shh - currently unused + payer; + borrower; + repayAmount; + + if (!markets[cToken].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates repayBorrow and reverts on rejection. May emit logs. + * @param cToken Asset being repaid + * @param payer The address repaying the borrow + * @param borrower The address of the borrower + * @param actualRepayAmount The amount of underlying being repaid + */ + function repayBorrowVerify( + address cToken, + address payer, + address borrower, + uint actualRepayAmount, + uint borrowerIndex) external { + // Shh - currently unused + cToken; + payer; + borrower; + actualRepayAmount; + borrowerIndex; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the liquidation should be allowed to occur + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param repayAmount The amount of underlying being repaid + */ + function liquidateBorrowAllowed( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint repayAmount) external returns (uint) { + // Shh - currently unused + liquidator; + + if (!markets[cTokenBorrowed].isListed || !markets[cTokenCollateral].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + // *may include Policy Hook-type checks + + /* The borrower must have shortfall in order to be liquidatable */ + (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower); + if (err != Error.NO_ERROR) { + return uint(err); + } + if (shortfall == 0) { + return uint(Error.INSUFFICIENT_SHORTFALL); + } + + /* The liquidator may not repay more than what is allowed by the closeFactor */ + uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower); + (MathError mathErr, uint maxClose) = mulScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance); + if (mathErr != MathError.NO_ERROR) { + return uint(Error.MATH_ERROR); + } + if (repayAmount > maxClose) { + return uint(Error.TOO_MUCH_REPAY); + } + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates liquidateBorrow and reverts on rejection. May emit logs. + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param actualRepayAmount The amount of underlying being repaid + */ + function liquidateBorrowVerify( + address cTokenBorrowed, + address cTokenCollateral, + address liquidator, + address borrower, + uint actualRepayAmount, + uint seizeTokens) external { + // Shh - currently unused + cTokenBorrowed; + cTokenCollateral; + liquidator; + borrower; + actualRepayAmount; + seizeTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the seizing of assets should be allowed to occur + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param seizeTokens The number of collateral tokens to seize + */ + function seizeAllowed( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!seizeGuardianPaused, "seize is paused"); + + // Shh - currently unused + liquidator; + borrower; + seizeTokens; + + if (!markets[cTokenCollateral].isListed || !markets[cTokenBorrowed].isListed) { + return uint(Error.MARKET_NOT_LISTED); + } + + if (CToken(cTokenCollateral).comptroller() != CToken(cTokenBorrowed).comptroller()) { + return uint(Error.COMPTROLLER_MISMATCH); + } + + // *may include Policy Hook-type checks + + return uint(Error.NO_ERROR); + } + + /** + * @notice Validates seize and reverts on rejection. May emit logs. + * @param cTokenCollateral Asset which was used as collateral and will be seized + * @param cTokenBorrowed Asset which was borrowed by the borrower + * @param liquidator The address repaying the borrow and seizing the collateral + * @param borrower The address of the borrower + * @param seizeTokens The number of collateral tokens to seize + */ + function seizeVerify( + address cTokenCollateral, + address cTokenBorrowed, + address liquidator, + address borrower, + uint seizeTokens) external { + // Shh - currently unused + cTokenCollateral; + cTokenBorrowed; + liquidator; + borrower; + seizeTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /** + * @notice Checks if the account should be allowed to transfer tokens in the given market + * @param cToken The market to verify the transfer against + * @param src The account which sources the tokens + * @param dst The account which receives the tokens + * @param transferTokens The number of cTokens to transfer + * @return 0 if the transfer is allowed, otherwise a semi-opaque error code (See ErrorReporter.sol) + */ + function transferAllowed(address cToken, address src, address dst, uint transferTokens) external returns (uint) { + // Pausing is a very serious situation - we revert to sound the alarms + require(!transferGuardianPaused, "transfer is paused"); + + // Shh - currently unused + dst; + + // *may include Policy Hook-type checks + + // Currently the only consideration is whether or not + // the src is allowed to redeem this many tokens + return redeemAllowedInternal(cToken, src, transferTokens); + } + + /** + * @notice Validates transfer and reverts on rejection. May emit logs. + * @param cToken Asset being transferred + * @param src The account which sources the tokens + * @param dst The account which receives the tokens + * @param transferTokens The number of cTokens to transfer + */ + function transferVerify(address cToken, address src, address dst, uint transferTokens) external { + // Shh - currently unused + cToken; + src; + dst; + transferTokens; + + // Shh - we don't ever want this hook to be marked pure + if (false) { + maxAssets = maxAssets; + } + } + + /*** Liquidity/Liquidation Calculations ***/ + + /** + * @dev Local vars for avoiding stack-depth limits in calculating account liquidity. + * Note that `cTokenBalance` is the number of cTokens the account owns in the market, + * whereas `borrowBalance` is the amount of underlying that the account has borrowed. + */ + struct AccountLiquidityLocalVars { + uint sumCollateral; + uint sumBorrowPlusEffects; + uint cTokenBalance; + uint borrowBalance; + uint exchangeRateMantissa; + uint oraclePriceMantissa; + Exp collateralFactor; + Exp exchangeRate; + Exp oraclePrice; + Exp tokensToEther; + } + + /** + * @notice Determine the current account liquidity wrt collateral requirements + * @return (possible error code (semi-opaque), + account liquidity in excess of collateral requirements, + * account shortfall below collateral requirements) + */ + function getAccountLiquidity(address account) public view returns (uint, uint, uint) { + (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); + + return (uint(err), liquidity, shortfall); + } + + /** + * @notice Determine the current account liquidity wrt collateral requirements + * @return (possible error code, + account liquidity in excess of collateral requirements, + * account shortfall below collateral requirements) + */ + function getAccountLiquidityInternal(address account) internal view returns (Error, uint, uint) { + return getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); + } + + /** + * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed + * @param cTokenModify The market to hypothetically redeem/borrow in + * @param account The account to determine liquidity for + * @param redeemTokens The number of tokens to hypothetically redeem + * @param borrowAmount The amount of underlying to hypothetically borrow + * @return (possible error code (semi-opaque), + hypothetical account liquidity in excess of collateral requirements, + * hypothetical account shortfall below collateral requirements) + */ + function getHypotheticalAccountLiquidity( + address account, + address cTokenModify, + uint redeemTokens, + uint borrowAmount) public view returns (uint, uint, uint) { + (Error err, uint liquidity, uint shortfall) = getHypotheticalAccountLiquidityInternal(account, CToken(cTokenModify), redeemTokens, borrowAmount); + return (uint(err), liquidity, shortfall); + } + + /** + * @notice Determine what the account liquidity would be if the given amounts were redeemed/borrowed + * @param cTokenModify The market to hypothetically redeem/borrow in + * @param account The account to determine liquidity for + * @param redeemTokens The number of tokens to hypothetically redeem + * @param borrowAmount The amount of underlying to hypothetically borrow + * @dev Note that we calculate the exchangeRateStored for each collateral cToken using stored data, + * without calculating accumulated interest. + * @return (possible error code, + hypothetical account liquidity in excess of collateral requirements, + * hypothetical account shortfall below collateral requirements) + */ + function getHypotheticalAccountLiquidityInternal( + address account, + CToken cTokenModify, + uint redeemTokens, + uint borrowAmount) internal view returns (Error, uint, uint) { + + AccountLiquidityLocalVars memory vars; // Holds all our calculation results + uint oErr; + MathError mErr; + + // For each asset the account is in + CToken[] memory assets = accountAssets[account]; + for (uint i = 0; i < assets.length; i++) { + CToken asset = assets[i]; + + // Read the balances and exchange rate from the cToken + (oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account); + if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades + return (Error.SNAPSHOT_ERROR, 0, 0); + } + vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa}); + vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa}); + + // Get the normalized price of the asset + vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset); + if (vars.oraclePriceMantissa == 0) { + return (Error.PRICE_ERROR, 0, 0); + } + vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa}); + + // Pre-compute a conversion factor from tokens -> ether (normalized price value) + (mErr, vars.tokensToEther) = mulExp3(vars.collateralFactor, vars.exchangeRate, vars.oraclePrice); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // sumCollateral += tokensToEther * cTokenBalance + (mErr, vars.sumCollateral) = mulScalarTruncateAddUInt(vars.tokensToEther, vars.cTokenBalance, vars.sumCollateral); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // sumBorrowPlusEffects += oraclePrice * borrowBalance + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // Calculate effects of interacting with cTokenModify + if (asset == cTokenModify) { + // redeem effect + // sumBorrowPlusEffects += tokensToEther * redeemTokens + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.tokensToEther, redeemTokens, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + + // borrow effect + // sumBorrowPlusEffects += oraclePrice * borrowAmount + (mErr, vars.sumBorrowPlusEffects) = mulScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects); + if (mErr != MathError.NO_ERROR) { + return (Error.MATH_ERROR, 0, 0); + } + } + } + + // These are safe, as the underflow condition is checked first + if (vars.sumCollateral > vars.sumBorrowPlusEffects) { + return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0); + } else { + return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral); + } + } + + /** + * @notice Calculate number of tokens of collateral asset to seize given an underlying amount + * @dev Used in liquidation (called in cToken.liquidateBorrowFresh) + * @param cTokenBorrowed The address of the borrowed cToken + * @param cTokenCollateral The address of the collateral cToken + * @param actualRepayAmount The amount of cTokenBorrowed underlying to convert into cTokenCollateral tokens + * @return (errorCode, number of cTokenCollateral tokens to be seized in a liquidation) + */ + function liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint actualRepayAmount) external view returns (uint, uint) { + /* Read oracle prices for borrowed and collateral markets */ + uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed)); + uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral)); + if (priceBorrowedMantissa == 0 || priceCollateralMantissa == 0) { + return (uint(Error.PRICE_ERROR), 0); + } + + /* + * Get the exchange rate and calculate the number of collateral tokens to seize: + * seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral + * seizeTokens = seizeAmount / exchangeRate + * = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate) + */ + uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error + uint seizeTokens; + Exp memory numerator; + Exp memory denominator; + Exp memory ratio; + MathError mathErr; + + (mathErr, numerator) = mulExp(liquidationIncentiveMantissa, priceBorrowedMantissa); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, denominator) = mulExp(priceCollateralMantissa, exchangeRateMantissa); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, ratio) = divExp(numerator, denominator); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + (mathErr, seizeTokens) = mulScalarTruncate(ratio, actualRepayAmount); + if (mathErr != MathError.NO_ERROR) { + return (uint(Error.MATH_ERROR), 0); + } + + return (uint(Error.NO_ERROR), seizeTokens); + } + + /*** Admin Functions ***/ + + /** + * @notice Sets a new price oracle for the comptroller + * @dev Admin function to set a new price oracle + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setPriceOracle(PriceOracle newOracle) public returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PRICE_ORACLE_OWNER_CHECK); + } + + // Track the old oracle for the comptroller + PriceOracle oldOracle = oracle; + + // Set comptroller's oracle to newOracle + oracle = newOracle; + + // Emit NewPriceOracle(oldOracle, newOracle) + emit NewPriceOracle(oldOracle, newOracle); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets the closeFactor used when liquidating borrows + * @dev Admin function to set closeFactor + * @param newCloseFactorMantissa New close factor, scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_CLOSE_FACTOR_OWNER_CHECK); + } + + Exp memory newCloseFactorExp = Exp({mantissa: newCloseFactorMantissa}); + Exp memory lowLimit = Exp({mantissa: closeFactorMinMantissa}); + if (lessThanOrEqualExp(newCloseFactorExp, lowLimit)) { + return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); + } + + Exp memory highLimit = Exp({mantissa: closeFactorMaxMantissa}); + if (lessThanExp(highLimit, newCloseFactorExp)) { + return fail(Error.INVALID_CLOSE_FACTOR, FailureInfo.SET_CLOSE_FACTOR_VALIDATION); + } + + uint oldCloseFactorMantissa = closeFactorMantissa; + closeFactorMantissa = newCloseFactorMantissa; + emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets the collateralFactor for a market + * @dev Admin function to set per-market collateralFactor + * @param cToken The market to set the factor on + * @param newCollateralFactorMantissa The new collateral factor, scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setCollateralFactor(CToken cToken, uint newCollateralFactorMantissa) external returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_COLLATERAL_FACTOR_OWNER_CHECK); + } + + // Verify market is listed + Market storage market = markets[address(cToken)]; + if (!market.isListed) { + return fail(Error.MARKET_NOT_LISTED, FailureInfo.SET_COLLATERAL_FACTOR_NO_EXISTS); + } + + Exp memory newCollateralFactorExp = Exp({mantissa: newCollateralFactorMantissa}); + + // Check collateral factor <= 0.9 + Exp memory highLimit = Exp({mantissa: collateralFactorMaxMantissa}); + if (lessThanExp(highLimit, newCollateralFactorExp)) { + return fail(Error.INVALID_COLLATERAL_FACTOR, FailureInfo.SET_COLLATERAL_FACTOR_VALIDATION); + } + + // If collateral factor != 0, fail if price == 0 + if (newCollateralFactorMantissa != 0 && oracle.getUnderlyingPrice(cToken) == 0) { + return fail(Error.PRICE_ERROR, FailureInfo.SET_COLLATERAL_FACTOR_WITHOUT_PRICE); + } + + // Set market's collateral factor to new collateral factor, remember old value + uint oldCollateralFactorMantissa = market.collateralFactorMantissa; + market.collateralFactorMantissa = newCollateralFactorMantissa; + + // Emit event with asset, old collateral factor, and new collateral factor + emit NewCollateralFactor(cToken, oldCollateralFactorMantissa, newCollateralFactorMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets maxAssets which controls how many markets can be entered + * @dev Admin function to set maxAssets + * @param newMaxAssets New max assets + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setMaxAssets(uint newMaxAssets) external returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_MAX_ASSETS_OWNER_CHECK); + } + + uint oldMaxAssets = maxAssets; + maxAssets = newMaxAssets; + emit NewMaxAssets(oldMaxAssets, newMaxAssets); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Sets liquidationIncentive + * @dev Admin function to set liquidationIncentive + * @param newLiquidationIncentiveMantissa New liquidationIncentive scaled by 1e18 + * @return uint 0=success, otherwise a failure. (See ErrorReporter for details) + */ + function _setLiquidationIncentive(uint newLiquidationIncentiveMantissa) external returns (uint) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_LIQUIDATION_INCENTIVE_OWNER_CHECK); + } + + // Check de-scaled min <= newLiquidationIncentive <= max + Exp memory newLiquidationIncentive = Exp({mantissa: newLiquidationIncentiveMantissa}); + Exp memory minLiquidationIncentive = Exp({mantissa: liquidationIncentiveMinMantissa}); + if (lessThanExp(newLiquidationIncentive, minLiquidationIncentive)) { + return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); + } + + Exp memory maxLiquidationIncentive = Exp({mantissa: liquidationIncentiveMaxMantissa}); + if (lessThanExp(maxLiquidationIncentive, newLiquidationIncentive)) { + return fail(Error.INVALID_LIQUIDATION_INCENTIVE, FailureInfo.SET_LIQUIDATION_INCENTIVE_VALIDATION); + } + + // Save current value for use in log + uint oldLiquidationIncentiveMantissa = liquidationIncentiveMantissa; + + // Set liquidation incentive to new incentive + liquidationIncentiveMantissa = newLiquidationIncentiveMantissa; + + // Emit event with old incentive, new incentive + emit NewLiquidationIncentive(oldLiquidationIncentiveMantissa, newLiquidationIncentiveMantissa); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Add the market to the markets mapping and set it as listed + * @dev Admin function to set isListed and add support for the market + * @param cToken The address of the market (token) to list + * @return uint 0=success, otherwise a failure. (See enum Error for details) + */ + function _supportMarket(CToken cToken) external returns (uint) { + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SUPPORT_MARKET_OWNER_CHECK); + } + + if (markets[address(cToken)].isListed) { + return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS); + } + + cToken.isCToken(); // Sanity check to make sure its really a CToken + + markets[address(cToken)] = Market({isListed: true, collateralFactorMantissa: 0}); + emit MarketListed(cToken); + + return uint(Error.NO_ERROR); + } + + /** + * @notice Admin function to change the Pause Guardian + * @param newPauseGuardian The address of the new Pause Guardian + * @return uint 0=success, otherwise a failure. (See enum Error for details) + */ + function _setPauseGuardian(address newPauseGuardian) public returns (uint) { + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PAUSE_GUARDIAN_OWNER_CHECK); + } + + // Save current value for inclusion in log + address oldPauseGuardian = pauseGuardian; + + // Store pauseGuardian with value newPauseGuardian + pauseGuardian = newPauseGuardian; + + // Emit NewPauseGuardian(OldPauseGuardian, NewPauseGuardian) + emit NewPauseGuardian(oldPauseGuardian, pauseGuardian); + + return uint(Error.NO_ERROR); + } + + function _setMintPaused(CToken cToken, bool state) public returns (bool) { + require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + mintGuardianPaused[address(cToken)] = state; + emit ActionPaused(cToken, "Mint", state); + return state; + } + + function _setBorrowPaused(CToken cToken, bool state) public returns (bool) { + require(markets[address(cToken)].isListed, "cannot pause a market that is not listed"); + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + borrowGuardianPaused[address(cToken)] = state; + emit ActionPaused(cToken, "Borrow", state); + return state; + } + + function _setTransferPaused(bool state) public returns (bool) { + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + transferGuardianPaused = state; + emit ActionPaused("Transfer", state); + return state; + } + + function _setSeizePaused(bool state) public returns (bool) { + require(msg.sender == pauseGuardian || msg.sender == admin, "only pause guardian and admin can pause"); + require(msg.sender == admin || state == true, "only admin can unpause"); + + seizeGuardianPaused = state; + emit ActionPaused("Seize", state); + return state; + } + + function _become(Unitroller unitroller) public { + require(msg.sender == unitroller.admin(), "only unitroller admin can change brains"); + + uint changeStatus = unitroller._acceptImplementation(); + require(changeStatus == 0, "change not authorized"); + } +} diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index a691a8d4c..bb27ac95e 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -94,3 +94,37 @@ contract ComptrollerV2Storage is ComptrollerV1Storage { mapping(address => bool) public mintGuardianPaused; mapping(address => bool) public borrowGuardianPaused; } + +contract ComptrollerV3Storage is ComptrollerV2Storage { + struct CompMarketState { + /// @notice The market's last updated compBorrowIndex or compSupplyIndex + uint224 index; + + /// @notice The block number the index was last updated at + uint32 block; + } + + /// @notice A list of the markets earning COMP + CToken[] public compMarkets; + + /// @notice The rate at which the flywheel distributes COMP, per block + uint public compRate; + + /// @notice The portion of compRate that each market currently receives + mapping(address => uint) public compSpeeds; + + /// @notice The COMP market supply state for each market + mapping(address => CompMarketState) public compSupplyState; + + /// @notice The COMP market borrow state for each market + mapping(address => CompMarketState) public compBorrowState; + + /// @notice The COMP borrow index for each supplier for each market as of the last time they accrued COMP + mapping(address => mapping(address => uint)) public compSupplierIndex; + + /// @notice The COMP borrow index for each borrower for each market as of the last time they accrued COMP + mapping(address => mapping(address => uint)) public compBorrowerIndex; + + /// @notice The COMP accrued but not yet transferred to each user + mapping(address => uint) public compAccrued; +} diff --git a/contracts/Exponential.sol b/contracts/Exponential.sol index f4bab1ba8..b92d68562 100644 --- a/contracts/Exponential.sol +++ b/contracts/Exponential.sol @@ -11,6 +11,7 @@ import "./CarefulMath.sol"; */ contract Exponential is CarefulMath { uint constant expScale = 1e18; + uint constant doubleScale = 1e36; uint constant halfExpScale = expScale/2; uint constant mantissaOne = expScale; @@ -18,6 +19,10 @@ contract Exponential is CarefulMath { uint mantissa; } + struct Double { + uint mantissa; + } + /** * @dev Creates an exponential from numerator and denominator values. * Note: Returns an error if (`num` * 10e18) > MAX_INT, @@ -223,4 +228,123 @@ contract Exponential is CarefulMath { function isZeroExp(Exp memory value) pure internal returns (bool) { return value.mantissa == 0; } + + function safe224(uint n, string memory errorMessage) pure internal returns (uint224) { + require(n < 2**224, errorMessage); + return uint224(n); + } + + function safe32(uint n, string memory errorMessage) pure internal returns (uint32) { + require(n < 2**32, errorMessage); + return uint32(n); + } + + function add_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: add_(a.mantissa, b.mantissa)}); + } + + function add_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: add_(a.mantissa, b.mantissa)}); + } + + function add_(uint a, uint b) pure internal returns (uint) { + return add_(a, b, "addition overflow"); + } + + function add_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + uint c = a + b; + require(c >= a, errorMessage); + return c; + } + + function sub_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: sub_(a.mantissa, b.mantissa)}); + } + + function sub_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: sub_(a.mantissa, b.mantissa)}); + } + + function sub_(uint a, uint b) pure internal returns (uint) { + return sub_(a, b, "subtraction underflow"); + } + + function sub_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + require(b <= a, errorMessage); + return a - b; + } + + function mul_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: mul_(a.mantissa, b.mantissa) / expScale}); + } + + function mul_(Exp memory a, uint b) pure internal returns (Exp memory) { + return Exp({mantissa: mul_(a.mantissa, b)}); + } + + function mul_(uint a, Exp memory b) pure internal returns (uint) { + return mul_(a, b.mantissa) / expScale; + } + + function mul_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: mul_(a.mantissa, b.mantissa) / doubleScale}); + } + + function mul_(Double memory a, uint b) pure internal returns (Double memory) { + return Double({mantissa: mul_(a.mantissa, b)}); + } + + function mul_(uint a, Double memory b) pure internal returns (uint) { + return mul_(a, b.mantissa) / doubleScale; + } + + function mul_(uint a, uint b) pure internal returns (uint) { + return mul_(a, b, "multiplication overflow"); + } + + function mul_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + if (a == 0 || b == 0) { + return 0; + } + uint c = a * b; + require(c / a == b, errorMessage); + return c; + } + + function div_(Exp memory a, Exp memory b) pure internal returns (Exp memory) { + return Exp({mantissa: div_(mul_(a.mantissa, expScale), b.mantissa)}); + } + + function div_(Exp memory a, uint b) pure internal returns (Exp memory) { + return Exp({mantissa: div_(a.mantissa, b)}); + } + + function div_(uint a, Exp memory b) pure internal returns (uint) { + return div_(mul_(a, expScale), b.mantissa); + } + + function div_(Double memory a, Double memory b) pure internal returns (Double memory) { + return Double({mantissa: div_(mul_(a.mantissa, doubleScale), b.mantissa)}); + } + + function div_(Double memory a, uint b) pure internal returns (Double memory) { + return Double({mantissa: div_(a.mantissa, b)}); + } + + function div_(uint a, Double memory b) pure internal returns (uint) { + return div_(mul_(a, doubleScale), b.mantissa); + } + + function div_(uint a, uint b) pure internal returns (uint) { + return div_(a, b, "divide by zero"); + } + + function div_(uint a, uint b, string memory errorMessage) pure internal returns (uint) { + require(b > 0, errorMessage); + return a / b; + } + + function fraction(uint a, uint b) pure internal returns (Double memory) { + return Double({mantissa: div_(mul_(a, doubleScale), b)}); + } } diff --git a/contracts/Flywheel/Dripper.sol b/contracts/Flywheel/Dripper.sol new file mode 100644 index 000000000..89b708fdd --- /dev/null +++ b/contracts/Flywheel/Dripper.sol @@ -0,0 +1,100 @@ +pragma solidity ^0.5.16; + +/** + * @title Dripper Contract + * @notice Distributes a token to a different contract at a fixed rate. + * @dev This contract must be poked via the `drip()` function every so often. + * @author Compound + */ +contract Dripper { + + /// @notice The block number when the Dripper started (immutable) + uint public dripStart; + + /// @notice Tokens per block that to drip to target (immutable) + uint public dripRate; + + /// @notice Reference to token to drip (immutable) + EIP20Interface public token; + + /// @notice Target to receive dripped tokens (immutable) + address public target; + + /// @notice Amount that has already been dripped + uint public dripped; + + /** + * @notice Constructs a Dripper + * @param dripRate_ Numer of tokens per block to drip + * @param token_ The token to drip + * @param target_ The recipient of dripped tokens + */ + constructor(uint dripRate_, EIP20Interface token_, address target_) public { + dripStart = block.number; + dripRate = dripRate_; + token = token_; + target = target_; + dripped = 0; + } + + /** + * @notice Drips the maximum amount of tokens to match the drip rate since inception + * @dev Note: this will only drip up to the amount of tokens available. + * @return The amount of tokens dripped in this call + */ + function drip() public returns (uint) { + // First, read storage into memory + EIP20Interface token_ = token; + uint dripperBalance_ = token_.balanceOf(address(this)); // TODO: Verify this is a static call + uint dripRate_ = dripRate; + uint dripStart_ = dripStart; + uint dripped_ = dripped; + address target_ = target; + uint blockNumber_ = block.number; + + // Next, calculate intermediate values + uint dripTotal_ = mul(dripRate_, blockNumber_ - dripStart_, "dripTotal overflow"); + uint deltaDrip_ = sub(dripTotal_, dripped_, "deltaDrip underflow"); + uint toDrip_ = min(dripperBalance_, deltaDrip_); + uint drippedNext_ = add(dripped_, toDrip_, "tautological"); + + // Finally, write new `dripped` value and transfer tokens to target + dripped = drippedNext_; + token_.transfer(target_, toDrip_); + + return toDrip_; + } + + /* Internal helper functions for safe math */ + + function add(uint a, uint b, string memory errorMessage) internal pure returns (uint) { + uint c = a + b; + require(c >= a, errorMessage); + return c; + } + + function sub(uint a, uint b, string memory errorMessage) internal pure returns (uint) { + require(b <= a, errorMessage); + uint c = a - b; + return c; + } + + function mul(uint a, uint b, string memory errorMessage) internal pure returns (uint) { + if (a == 0) { + return 0; + } + uint c = a * b; + require(c / a == b, errorMessage); + return c; + } + + function min(uint a, uint b) internal pure returns (uint) { + if (a <= b) { + return a; + } else { + return b; + } + } +} + +import "../EIP20Interface.sol"; diff --git a/docs/Dripper.pdf b/docs/Dripper.pdf new file mode 100644 index 000000000..671c1df60 Binary files /dev/null and b/docs/Dripper.pdf differ diff --git a/scenario/src/Builder/ComptrollerImplBuilder.ts b/scenario/src/Builder/ComptrollerImplBuilder.ts index 1799aafeb..c737f491b 100644 --- a/scenario/src/Builder/ComptrollerImplBuilder.ts +++ b/scenario/src/Builder/ComptrollerImplBuilder.ts @@ -8,10 +8,14 @@ import { Arg, Fetcher, getFetcherValue } from '../Command'; import { storeAndSaveContract } from '../Networks'; import { getContract, getTestContract } from '../Contract'; -const ComptrollerContract = getContract('Comptroller'); const ComptrollerG1Contract = getContract('ComptrollerG1'); const ComptrollerScenarioG1Contract = getTestContract('ComptrollerScenarioG1'); + +const ComptrollerG2Contract = getContract('ComptrollerG2'); + const ComptrollerScenarioContract = getTestContract('ComptrollerScenario'); +const ComptrollerContract = getContract('Comptroller'); + const ComptrollerBorkedContract = getTestContract('ComptrollerBorked'); export interface ComptrollerImplData { @@ -61,6 +65,24 @@ export async function buildComptrollerImpl( }) ), new Fetcher<{ name: StringV }, ComptrollerImplData>( + ` + #### StandardG2 + + * "StandardG2 name:" - The standard generation 2 Comptroller contract + * E.g. "Comptroller Deploy StandardG2 MyStandard" + `, + 'StandardG2', + [new Arg('name', getStringV)], + async (world, { name }) => { + return { + invokation: await ComptrollerG2Contract.deploy(world, from, []), + name: name.val, + contract: 'ComptrollerG2', + description: 'StandardG2 Comptroller Impl' + }; + } + ), + new Fetcher<{ name: StringV }, ComptrollerImplData>( ` #### StandardG1 diff --git a/scenario/src/Contract/Comptroller.ts b/scenario/src/Contract/Comptroller.ts index 5afa8ede5..b6557b8e3 100644 --- a/scenario/src/Contract/Comptroller.ts +++ b/scenario/src/Contract/Comptroller.ts @@ -45,6 +45,7 @@ interface ComptrollerMethods { seizeGuardianPaused(): Callable mintGuardianPaused(market: string): Callable borrowGuardianPaused(market: string): Callable + getCompMarkets(): Callable } export interface Comptroller extends Contract { diff --git a/scenario/src/Contract/ComptrollerImpl.ts b/scenario/src/Contract/ComptrollerImpl.ts index 728d2fc5d..9c3556456 100644 --- a/scenario/src/Contract/ComptrollerImpl.ts +++ b/scenario/src/Contract/ComptrollerImpl.ts @@ -10,6 +10,12 @@ interface ComptrollerImplMethods { closeFactor?: encodedNumber, reinitializing?: boolean ): Sendable; + + _become( + comptroller: string, + compRate: encodedNumber, + compMarkets: string[], + ): Sendable; } export interface ComptrollerImpl extends Contract { diff --git a/scenario/src/Contract/Dripper.ts b/scenario/src/Contract/Dripper.ts new file mode 100644 index 000000000..bf01079cb --- /dev/null +++ b/scenario/src/Contract/Dripper.ts @@ -0,0 +1,17 @@ +import { Contract } from '../Contract'; +import { encodedNumber } from '../Encoding'; +import { Callable, Sendable } from '../Invokation'; + +export interface DripperMethods { + drip(): Sendable; + dripped(): Callable; + dripStart(): Callable; + dripRate(): Callable; + token(): Callable; + target(): Callable; +} + +export interface Dripper extends Contract { + methods: DripperMethods; + name: string; +} diff --git a/scenario/src/CoreEvent.ts b/scenario/src/CoreEvent.ts index 75e57a3c6..e521d6620 100644 --- a/scenario/src/CoreEvent.ts +++ b/scenario/src/CoreEvent.ts @@ -41,6 +41,7 @@ import { fork } from './Hypothetical'; import { buildContractEvent } from './EventBuilder'; import { Counter } from './Contract/Counter'; import { CompoundLens } from './Contract/CompoundLens'; +import { Dripper } from './Contract/Dripper'; import Web3 from 'web3'; export class EventProcessingError extends Error { @@ -415,7 +416,7 @@ export const commands: (View | ((world: World) => Promise>))[] = } ), - new View<{ blockNumber: NumberV }>( + new Command<{ blockNumber: NumberV }>( ` #### SetBlockNumber @@ -424,14 +425,31 @@ export const commands: (View | ((world: World) => Promise>))[] = `, 'SetBlockNumber', [new Arg('blockNumber', getNumberV)], - async (world, { blockNumber }) => { - - await sendRPC(world, 'evm_mineBlockNumber', [blockNumber.val]) + async (world, from, { blockNumber }) => { + await sendRPC(world, 'evm_mineBlockNumber', [blockNumber.toNumber() - 1]) return world; } ), - new View<{ blockNumber: NumberV }>( + new Command<{ blockNumber: NumberV, event: EventV }>( + ` + #### Block + + * "Block 10 (...event)" - Set block to block N and run event + * E.g. "Block 10 (Comp Deploy Admin)" + `, + 'Block', + [ + new Arg('blockNumber', getNumberV), + new Arg('event', getEventV) + ], + async (world, from, { blockNumber, event }) => { + await sendRPC(world, 'evm_mineBlockNumber', [blockNumber.toNumber() - 2]) + return await processCoreEvent(world, event.val, from); + } + ), + + new Command<{ blockNumber: NumberV }>( ` #### AdvanceBlocks @@ -440,7 +458,7 @@ export const commands: (View | ((world: World) => Promise>))[] = `, 'AdvanceBlocks', [new Arg('blockNumber', getNumberV)], - async (world, { blockNumber }) => { + async (world, from, { blockNumber }) => { const currentBlockNumber = await getCurrentBlockNumber(world); await sendRPC(world, 'evm_mineBlockNumber', [Number(blockNumber.val) + currentBlockNumber]); return world; @@ -792,8 +810,9 @@ export const commands: (View | ((world: World) => Promise>))[] = { subExpressions: governorCommands() } ), - buildContractEvent("Counter"), - buildContractEvent("CompoundLens"), + buildContractEvent("Counter", false), + buildContractEvent("CompoundLens", false), + buildContractEvent("Dripper", true), new View<{ event: EventV }>( ` diff --git a/scenario/src/CoreValue.ts b/scenario/src/CoreValue.ts index 8d420eba3..b6e07f95c 100644 --- a/scenario/src/CoreValue.ts +++ b/scenario/src/CoreValue.ts @@ -975,8 +975,9 @@ const fetchers = [ ]; let contractFetchers = [ - "Counter", - "CompoundLens" + { contract: "Counter", implicit: false }, + { contract: "CompoundLens", implicit: false }, + { contract: "Dripper", implicit: true } ]; export async function getFetchers(world: World) { @@ -984,8 +985,8 @@ export async function getFetchers(world: World) { return { world, fetchers: world.fetchers }; } - let allFetchers = fetchers.concat(await Promise.all(contractFetchers.map((contractName) => { - return buildContractFetcher(world, contractName); + let allFetchers = fetchers.concat(await Promise.all(contractFetchers.map(({contract, implicit}) => { + return buildContractFetcher(world, contract, implicit); }))); return { world: world.set('fetchers', allFetchers), fetchers: allFetchers }; diff --git a/scenario/src/Event/ComptrollerImplEvent.ts b/scenario/src/Event/ComptrollerImplEvent.ts index a0fc2d6b2..7097095df 100644 --- a/scenario/src/Event/ComptrollerImplEvent.ts +++ b/scenario/src/Event/ComptrollerImplEvent.ts @@ -3,8 +3,8 @@ import { addAction, describeUser, World } from '../World'; import { ComptrollerImpl } from '../Contract/ComptrollerImpl'; import { Unitroller } from '../Contract/Unitroller'; import { invoke } from '../Invokation'; -import { getAddressV, getEventV, getExpNumberV, getNumberV, getStringV } from '../CoreValue'; -import { AddressV, EventV, NumberV, StringV } from '../Value'; +import { getAddressV, getArrayV, getEventV, getExpNumberV, getNumberV, getStringV } from '../CoreValue'; +import { ArrayV, AddressV, EventV, NumberV, StringV } from '../Value'; import { Arg, Command, View, processCommandEvent } from '../Command'; import { buildComptrollerImpl } from '../Builder/ComptrollerImplBuilder'; import { ComptrollerErrorReporter } from '../ErrorReporter'; @@ -34,11 +34,13 @@ async function become( world: World, from: string, comptrollerImpl: ComptrollerImpl, - unitroller: Unitroller + unitroller: Unitroller, + compRate: encodedNumber, + compMarkets: string[], ): Promise { let invokation = await invoke( world, - comptrollerImpl.methods._become(unitroller._address), + comptrollerImpl.methods._become(unitroller._address, compRate, compMarkets), from, ComptrollerErrorReporter ); @@ -53,6 +55,7 @@ async function become( return world; } + async function mergeABI( world: World, from: string, @@ -67,6 +70,29 @@ async function mergeABI( return world; } +async function becomeG2( + world: World, + from: string, + comptrollerImpl: ComptrollerImpl, + unitroller: Unitroller +): Promise { + let invokation = await invoke( + world, + comptrollerImpl.methods._become(unitroller._address), + from, + ComptrollerErrorReporter + ); + + if (!world.dryRun) { + // Skip this specifically on dry runs since it's likely to crash due to a number of reasons + world = await mergeContractABI(world, 'Comptroller', unitroller, unitroller.name, comptrollerImpl.name); + } + + world = addAction(world, `Become ${unitroller._address}'s Comptroller Impl`, invokation); + + return world; +} + async function becomeG1( world: World, from: string, @@ -82,7 +108,6 @@ async function becomeG1( from, ComptrollerErrorReporter ); - if (!world.dryRun) { // Skip this specifically on dry runs since it's likely to crash due to a number of reasons world = await mergeContractABI(world, 'Comptroller', unitroller, unitroller.name, comptrollerImpl.name); @@ -96,7 +121,7 @@ async function becomeG1( return world; } -// Recome calls `become` on the G1 Comptroller, but passes a flag to not modify any of the initialization variables. +// Recome calls `become` on the G1 Comptroller, but passes a flag to not modify any of the initialization variables. async function recome( world: World, from: string, @@ -204,21 +229,47 @@ export function comptrollerImplCommands() { new Command<{ unitroller: Unitroller; comptrollerImpl: ComptrollerImpl; + }>( + ` + #### BecomeG2 + + * "ComptrollerImpl BecomeG2" - Become the comptroller, if possible. + * E.g. "ComptrollerImpl MyImpl BecomeG2 + `, + 'BecomeG2', + [ + new Arg('unitroller', getUnitroller, { implicit: true }), + new Arg('comptrollerImpl', getComptrollerImpl) + ], + (world, from, { unitroller, comptrollerImpl }) => becomeG2(world, from, comptrollerImpl, unitroller), + { namePos: 1 } + ), + + new Command<{ + unitroller: Unitroller; + comptrollerImpl: ComptrollerImpl; + compRate: NumberV; + compMarkets: ArrayV; }>( ` #### Become - * "ComptrollerImpl Become" - Become the comptroller, if possible. - * E.g. "ComptrollerImpl MyImpl Become + * "ComptrollerImpl Become " - Become the comptroller, if possible. + * E.g. "ComptrollerImpl MyImpl Become 0.1e18 [cDAI, cETH, cUSDC] `, 'Become', [ new Arg('unitroller', getUnitroller, { implicit: true }), - new Arg('comptrollerImpl', getComptrollerImpl) + new Arg('comptrollerImpl', getComptrollerImpl), + new Arg('compRate', getNumberV, { default: new NumberV(1e18) }), + new Arg('compMarkets', getArrayV(getAddressV)) ], - (world, from, { unitroller, comptrollerImpl }) => become(world, from, comptrollerImpl, unitroller), + (world, from, { unitroller, comptrollerImpl, compRate, compMarkets }) => { + return become(world, from, comptrollerImpl, unitroller, compRate.encode(), compMarkets.val.map(a => a.val)) + }, { namePos: 1 } ), + new Command<{ unitroller: Unitroller; comptrollerImpl: ComptrollerImpl; diff --git a/scenario/src/EventBuilder.ts b/scenario/src/EventBuilder.ts index f98118a92..e857193ef 100644 --- a/scenario/src/EventBuilder.ts +++ b/scenario/src/EventBuilder.ts @@ -7,7 +7,7 @@ import { storeAndSaveContract } from './Networks'; import { Contract, getContract } from './Contract'; import { getWorldContract } from './ContractLookup'; import { mustString } from './Utils'; -import { Callable, Sendable } from './Invokation'; +import { Callable, Sendable, invoke } from './Invokation'; import { AddressV, ArrayV, @@ -32,169 +32,238 @@ export interface ContractData { address?: string; } -async function getContractObject(world: World, event: Event): Promise { - return getWorldContract(world, [['Contracts', mustString(event)]]); -} +const typeMappings = () => ({ + address: { + builder: (x) => new AddressV(x), + getter: getAddressV + }, + 'address[]': { + builder: (x) => new ArrayV(x), + getter: (x) => getArrayV(x), + }, + string: { + builder: (x) => new StringV(x), + getter: getStringV + }, + uint256: { + builder: (x) => new NumberV(x), + getter: getNumberV + }, + 'uint256[]': { + builder: (x) => new ArrayV(x), + getter: (x) => getArrayV(x), + }, + 'uint32[]': { + builder: (x) => new ArrayV(x), + getter: (x) => getArrayV(x), + }, + 'uint96[]': { + builder: (x) => new ArrayV(x), + getter: (x) => getArrayV(x), + } +}); -export function buildContractEvent(contractName: string) { - let contractDeployer = getContract(contractName); - - async function build( - world: World, - from: string, - params: Event - ): Promise<{ world: World; contract: T; data: ContractData }> { - const fetchers = [ - new Fetcher<{ name: StringV }, ContractData>( - ` - #### ${contractName} - - * "${contractName} name:=${contractName}" - Build ${contractName} - * E.g. "${contractName} Deploy" - } - `, - contractName, - [ - new Arg('name', getStringV, { default: new StringV(contractName) }) - ], - async (world, { name }) => { - return { - invokation: await contractDeployer.deploy(world, from, []), - name: name.val, - contract: contractName - }; - }, - { catchall: true } - ) - ]; +function buildArg(contractName: string, name: string, input: AbiInput): Arg { + let { getter } = typeMappings()[input.type] || {}; - let data = await getFetcherValue>(`Deploy${contractName}`, fetchers, world, params); - let invokation = data.invokation; - delete data.invokation; + if (!getter) { + throw new Error(`Unknown ABI Input Type: ${input.type} of \`${name}\` in ${contractName}`); + } - if (invokation.error) { - throw invokation.error; - } + return new Arg(name, getter); +} - const contract = invokation.value!; - contract.address = contract._address; - const index = contractName == data.name ? [contractName] : [contractName, data.name]; - - world = await storeAndSaveContract( - world, - contract, - data.name, - invokation, - [ - { index: index, data: data } - ] - ); +function getEventName(s) { + return s.charAt(0).toUpperCase() + s.slice(1); +} - return { world, contract, data }; +function getContractObjectFn(contractName, implicit) { + if (implicit) { + return async function getContractObject(world: World): Promise { + return getWorldContract(world, [['Contracts', contractName]]); + } + } else { + return async function getContractObject(world: World, event: Event): Promise { + return getWorldContract(world, [['Contracts', mustString(event)]]); + } } +} +export function buildContractEvent(contractName: string, implicit) { + + + return async (world) => { + let contractDeployer = getContract(contractName); + let abis: AbiItem[] = await world.saddle.abi(contractName); + + async function build( + world: World, + from: string, + params: Event + ): Promise<{ world: World; contract: T; data: ContractData }> { + let constructors = abis.filter(({type}) => type === 'constructor'); + if (constructors.length === 0) { + constructors.push({ + constant: false, + inputs: [], + outputs: [], + payable: true, + stateMutability: "payable", + type: 'constructor' + }); + } - async function deploy(world: World, from: string, params: Event) { - let { world: nextWorld, contract, data } = await build(world, from, params); - world = nextWorld; + const fetchers = constructors.map((abi: any) => { + let nameArg = implicit ? [] : [ + new Arg('name', getStringV, { default: new StringV(contractName) }) + ]; + let nameArgDesc = implicit ? `` : `name:=${contractName}" ` + let inputNames = abi.inputs.map((input) => getEventName(input.name)); + let args = abi.inputs.map((input) => buildArg(contractName, input.name, input)); + return new Fetcher>( + ` + #### ${contractName} + + * "${contractName} ${nameArgDesc}${inputNames.join(" ")} - Build ${contractName} + * E.g. "${contractName} Deploy" + } + `, + contractName, + nameArg.concat(args), + async (world, paramValues) => { + let name = implicit ? contractName : paramValues['name'].val; + let params = args.map((arg) => paramValues[arg.name]); // TODO: This is just a guess + let paramsEncoded = params.map((param) => typeof(param['encode']) === 'function' ? param.encode() : param.val); + + return { + invokation: await contractDeployer.deploy(world, from, paramsEncoded), + name: name, + contract: contractName + }; + }, + { catchall: true } + ) + }); + + let data = await getFetcherValue>(`Deploy${contractName}`, fetchers, world, params); + let invokation = data.invokation; + delete data.invokation; + + if (invokation.error) { + throw invokation.error; + } - world = addAction( - world, - `Deployed ${contractName} ${data.contract} to address ${contract._address}`, - data.invokation - ); + const contract = invokation.value!; + contract.address = contract._address; + const index = contractName == data.name ? [contractName] : [contractName, data.name]; - return world; - } + world = await storeAndSaveContract( + world, + contract, + data.name, + invokation, + [ + { index: index, data: data } + ] + ); - function commands() { - return [ - new Command<{ params: EventV }>(` - #### ${contractName} + return { world, contract, data }; + } - * "${contractName} Deploy" - Deploy ${contractName} - * E.g. "Counter Deploy" - `, - "Deploy", - [ - new Arg("params", getEventV, { variadic: true }) - ], - (world, from, { params }) => deploy(world, from, params.val) - ) - ]; - } + async function deploy(world: World, from: string, params: Event) { + let { world: nextWorld, contract, data } = await build(world, from, params); + world = nextWorld; - async function processEvent(world: World, event: Event, from: string | null): Promise { - return await processCommandEvent(contractName, commands(), world, event, from); - } + world = addAction( + world, + `Deployed ${contractName} ${data.contract} to address ${contract._address}`, + data.invokation + ); - let command = new Command<{ event: EventV }>( - ` - #### ${contractName} + return world; + } - * "${contractName} ...event" - Runs given ${contractName} event - * E.g. "${contractName} Deploy" - `, - contractName, - [new Arg('event', getEventV, { variadic: true })], - (world, from, { event }) => { - return processEvent(world, event.val, from); - }, - { subExpressions: commands() } - ); - - return command; -} + function commands() { + async function buildOutput(world: World, from: string, fn: string, inputs: object, output: AbiItem): Promise { + const sendable = >(inputs['contract'].methods[fn](...Object.values(inputs).slice(1))); + let invokation = await invoke(world, sendable, from); -export async function buildContractFetcher(world: World, contractName: string) { + world = addAction( + world, + `Invokation of ${fn} with inputs ${inputs}`, + invokation + ); - let abis: AbiItem[] = await world.saddle.abi(contractName); + return world; + } - function fetchers() { - const typeMappings = { - address: { - builder: (x) => new AddressV(x), - getter: getAddressV - }, - 'address[]': { - builder: (x) => new ArrayV(x), - getter: (x) => getArrayV(x), - }, - string: { - builder: (x) => new StringV(x), - getter: getStringV - }, - uint256: { - builder: (x) => new NumberV(x), - getter: getNumberV - }, - 'uint256[]': { - builder: (x) => new ArrayV(x), - getter: (x) => getArrayV(x), - }, - 'uint32[]': { - builder: (x) => new ArrayV(x), - getter: (x) => getArrayV(x), + let abiCommands = abis.filter(({type}) => type === 'function').map((abi: any) => { + let eventName = getEventName(abi.name); + let inputNames = abi.inputs.map((input) => getEventName(input.name)); + let args = [ + new Arg("contract", getContractObjectFn(contractName, implicit), implicit ? { implicit: true } : {}) + ].concat(abi.inputs.map((input) => buildArg(contractName, abi.name, input))); + + return new Command(` + #### ${eventName} + + * "${eventName} ${inputNames.join(" ")}" - Executes \`${abi.name}\` function + `, + eventName, + args, + (world, from, inputs) => buildOutput(world, from, abi.name, inputs, abi.outputs[0]), + { namePos: implicit ? 0 : 1 } + ) + }); + + return [ + ...abiCommands, + new Command<{ params: EventV }>(` + #### ${contractName} + + * "${contractName} Deploy" - Deploy ${contractName} + * E.g. "Counter Deploy" + `, + "Deploy", + [ + new Arg("params", getEventV, { variadic: true }) + ], + (world, from, { params }) => deploy(world, from, params.val) + ) + ]; + } + + async function processEvent(world: World, event: Event, from: string | null): Promise { + return await processCommandEvent(contractName, commands(), world, event, from); + } + + let command = new Command<{ event: EventV }>( + ` + #### ${contractName} + + * "${contractName} ...event" - Runs given ${contractName} event + * E.g. "${contractName} Deploy" + `, + contractName, + [new Arg('event', getEventV, { variadic: true })], + (world, from, { event }) => { + return processEvent(world, event.val, from); }, - 'uint96[]': { - builder: (x) => new ArrayV(x), - getter: (x) => getArrayV(x), - } - }; + { subExpressions: commands() } + ); - function buildArg(name: string, input: AbiInput): Arg { - let { getter } = typeMappings[input.type] || {}; + return command; + } +} - if (!getter) { - throw new Error(`Unknown ABI Input Type: ${input.type} of \`${name}\` in ${contractName}`); - } +export async function buildContractFetcher(world: World, contractName: string, implicit: boolean) { - return new Arg(name, getter); - } + let abis: AbiItem[] = await world.saddle.abi(contractName); + function fetchers() { async function buildOutput(world: World, fn: string, inputs: object, output: AbiItem): Promise { const callable = >(inputs['contract'].methods[fn](...Object.values(inputs).slice(1))); let value = await callable.call(); - let { builder } = typeMappings[output.type] || {}; + let { builder } = typeMappings()[output.type] || {}; if (!builder) { throw new Error(`Unknown ABI Output Type: ${output.type} of \`${fn}\` in ${contractName}`); @@ -203,17 +272,12 @@ export async function buildContractFetcher(world: World, con return builder(value); } - return abis.map((abi: any) => { - function getEventName(s) { - return s.charAt(0).toUpperCase() + s.slice(1); - } - + return abis.filter(({name}) => !!name).map((abi: any) => { let eventName = getEventName(abi.name); let inputNames = abi.inputs.map((input) => getEventName(input.name)); let args = [ - new Arg("contract", getContractObject) - ].concat(abi.inputs.map((input) => buildArg(abi.name, input))); - + new Arg("contract", getContractObjectFn(contractName, implicit), implicit ? { implicit: true } : {}) + ].concat(abi.inputs.map((input) => buildArg(contractName, abi.name, input))); return new Fetcher(` #### ${eventName} @@ -222,7 +286,7 @@ export async function buildContractFetcher(world: World, con eventName, args, (world, inputs) => buildOutput(world, abi.name, inputs, abi.outputs[0]), - { namePos: 1 } + { namePos: implicit ? 0 : 1 } ) }); } @@ -231,7 +295,6 @@ export async function buildContractFetcher(world: World, con return await getFetcherValue(contractName, fetchers(), world, event); } - let fetcher = new Fetcher<{ res: Value }, Value>( ` #### ${contractName} diff --git a/scenario/src/Utils.ts b/scenario/src/Utils.ts index 1a0d525da..fb397b799 100644 --- a/scenario/src/Utils.ts +++ b/scenario/src/Utils.ts @@ -56,7 +56,7 @@ export function encodeABI(world: World, fnABI: string, fnParams: string[]): stri } export function encodeParameters(world: World, fnABI: string, fnParams: string[]): string { - const regex = /(\w+)\(([\w,]+)\)/; + const regex = /(\w+)\(([\w,\[\]]+)\)/; const res = regex.exec(fnABI); if (!res) { return '0x0'; @@ -66,7 +66,7 @@ export function encodeParameters(world: World, fnABI: string, fnParams: string[] } export function decodeParameters(world: World, fnABI: string, data: string): string[] { - const regex = /(\w+)\(([\w,]+)\)/; + const regex = /(\w+)\(([\w,\[\]]+)\)/; const res = regex.exec(fnABI); if (!res) { return []; diff --git a/scenario/src/Value/ComptrollerValue.ts b/scenario/src/Value/ComptrollerValue.ts index a249d1820..fe11047ab 100644 --- a/scenario/src/Value/ComptrollerValue.ts +++ b/scenario/src/Value/ComptrollerValue.ts @@ -92,6 +92,12 @@ async function getAssetsIn(world: World, comptroller: Comptroller, user: string) return new ListV(assetsList.map((a) => new AddressV(a))); } +async function getCompMarkets(world: World, comptroller: Comptroller): Promise { + let mkts = await comptroller.methods.getCompMarkets().call(); + + return new ListV(mkts.map((a) => new AddressV(a))); +} + async function checkListed(world: World, comptroller: Comptroller, cToken: CToken): Promise { let {0: isListed, 1: _collateralFactorMantissa} = await comptroller.methods.markets(cToken._address).call(); @@ -386,6 +392,16 @@ export function comptrollerFetchers() { ], async (world, {comptroller, cToken}) => new BoolV(await comptroller.methods.borrowGuardianPaused(cToken._address).call()) ), + new Fetcher<{comptroller: Comptroller}, ListV>(` + #### GetCompMarkets + + * "GetCompMarkets" - Returns an array of the currently enabled Comp markets. To use the auto-gen array getter compMarkets(uint), use CompMarkets + * E.g. "Comptroller GetCompMarkets" + `, + "GetCompMarkets", + [new Arg("comptroller", getComptroller, {implicit: true})], + async(world, {comptroller}) => await getCompMarkets(world, comptroller) + ) ]; } diff --git a/spec/scenario/CoreMacros b/spec/scenario/CoreMacros index 1d7575b6c..a57860852 100644 --- a/spec/scenario/CoreMacros +++ b/spec/scenario/CoreMacros @@ -15,18 +15,24 @@ Macro PricedComptroller closeFactor=0.1 maxAssets=20 ComptrollerImpl ScenComptrollerG1 BecomeG1 (PriceOracleProxy Address) closeFactor maxAssets ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ScenComptroller Become 1e18 [] Macro NewComptroller price=1.0 closeFactor=0.1 maxAssets=20 + --g1 Unitroller Deploy PriceOracle Deploy Fixed price ComptrollerImpl Deploy ScenarioG1 ScenComptrollerG1 Unitroller SetPendingImpl ScenComptrollerG1 PriceOracleProxy Deploy Admin (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) (Address Zero) (Address Zero) -- if listing cEther use ListedEtherToken to replace proxy ComptrollerImpl ScenComptrollerG1 BecomeG1 (PriceOracleProxy Address) closeFactor maxAssets + --g2 + ComptrollerImpl Deploy StandardG2 ComptrollerG2 + Unitroller SetPendingImpl ComptrollerG2 + ComptrollerImpl ComptrollerG2 BecomeG2 + --g3 ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ScenComptroller Become 1e18 [] Macro NewCToken erc20 cToken borrowRate=0.000005 initialExchangeRate=2e9 decimals=8 tokenType=Standard delegatorType=CErc20DelegatorScenario cTokenType=CErc20DelegateScenario admin=Admin becomeImplementationData="0x0" Erc20 Deploy tokenType erc20 erc20 diff --git a/spec/scenario/Flywheel/Dripper.scen b/spec/scenario/Flywheel/Dripper.scen new file mode 100644 index 000000000..6342299ab --- /dev/null +++ b/spec/scenario/Flywheel/Dripper.scen @@ -0,0 +1,84 @@ + +Test "Dripper is initialized correctly" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Assert Equal (Dripper Dripped) 0 + Assert Equal (Dripper DripStart) 100 + Assert Equal (Dripper DripRate) 5e18 + Assert Equal (Dripper Token) (Comp Address) + Assert Equal (Dripper Target) (Address Bank) + +Test "Dripper properly drips first drip" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Comp Transfer Dripper 5000e18 + SetBlockNumber 200 + Assert Equal (Dripper Drip) 500e18 + Assert Equal (Dripper Dripped) 0 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 500e18 + Assert Equal (Comp TokenBalance Bank) 500e18 + Assert Equal (Comp TokenBalance Dripper) 4500e18 + +Test "Dripper properly drips second drip" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Comp Transfer Dripper 5000e18 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 500e18 + SetBlockNumber 250 + Assert Equal (Dripper Drip) 250e18 + Block 250 (Dripper Drip) + Assert Equal (Dripper Dripped) 750e18 + Assert Equal (Comp TokenBalance Bank) 750e18 + Assert Equal (Comp TokenBalance Dripper) 4250e18 + +Test "Dripper properly drips zero sequentially" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Comp Transfer Dripper 5000e18 + Block 100 (Dripper Drip) + Assert Equal (Dripper Dripped) 0 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 500e18 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 500e18 + +Test "Dripper handles not having enough to drip" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Comp Transfer Dripper 200e18 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 200e18 + Assert Equal (Comp TokenBalance Bank) 200e18 + Assert Equal (Comp TokenBalance Dripper) 0 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 200e18 + Assert Equal (Comp TokenBalance Bank) 200e18 + Assert Equal (Comp TokenBalance Dripper) 0 + Comp Transfer Dripper 4800e18 + Block 200 (Dripper Drip) + Assert Equal (Dripper Dripped) 500e18 + Assert Equal (Comp TokenBalance Bank) 500e18 + Assert Equal (Comp TokenBalance Dripper) 4500e18 + Block 250 (Dripper Drip) + Assert Equal (Dripper Dripped) 750e18 + Assert Equal (Comp TokenBalance Bank) 750e18 + Assert Equal (Comp TokenBalance Dripper) 4250e18 + +Test "Revert on dripTotal overflow" + Comp Deploy Admin + Block 100 (Dripper Deploy 10e77 Comp Bank) + AllowFailures + Block 200 (Dripper Drip) + Assert Revert "revert dripTotal overflow" + +Test "Revert on deltaDrip underflow - reverses block!" + Comp Deploy Admin + Block 100 (Dripper Deploy 5e18 Comp Bank) + Comp Transfer Dripper 5000e18 + Assert Equal (Dripper DripStart) 100 + Block 200 (Dripper Drip) + AllowFailures + Block 100 (Dripper Drip) -- Going back in time! + Assert Revert "revert deltaDrip underflow" diff --git a/spec/scenario/Flywheel/Flywheel.scen b/spec/scenario/Flywheel/Flywheel.scen new file mode 100644 index 000000000..1d5a9ba1f --- /dev/null +++ b/spec/scenario/Flywheel/Flywheel.scen @@ -0,0 +1,56 @@ +-- NewComptroller, but with markets listed so that we can make them comp markets in constructor +Macro FlywheelComptroller price=1.0 closeFactor=0.1 maxAssets=20 + Unitroller Deploy + ---- g1 + PriceOracle Deploy Fixed price + ComptrollerImpl Deploy ScenarioG1 ScenComptrollerG1 + Unitroller SetPendingImpl ScenComptrollerG1 + PriceOracleProxy Deploy Admin (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) (Address Zero) (Address Zero) -- if listing cEther use ListedEtherToken to replace proxy + ComptrollerImpl ScenComptrollerG1 BecomeG1 (PriceOracleProxy Address) closeFactor maxAssets + --list some tokens + ListedCToken ZRX cZRX initialExchangeRate:1e9 + ListedCToken DAI cDAI initialExchangeRate:1e9 + ----g2 + ComptrollerImpl Deploy StandardG2 ComptrollerG2 --dont think we need scen + Unitroller SetPendingImpl ComptrollerG2 + ComptrollerImpl ComptrollerG2 BecomeG2 + -- final + ComptrollerImpl Deploy Scenario ComptrollerScen + Unitroller SetPendingImpl ComptrollerScen + ComptrollerImpl ComptrollerScen Become [cZRX cDAI] + +Pending "Accrue COMP during a mint" + +Pending "Accrue COMP during a borrow" + +Pending "Accrue COMP during a redeem" + +Pending "Accrue COMP during a repayBorrow" + +Pending "Accrue COMP during a repayBorrowBehalf" +--make sure it works with repay behalf 0 + +Pending "Accrue COMP during a liquidation" +--make sure to verify balance for both liquidator and borrower + +Pending "Accrue COMP during a transfer" +--make sure to verify balance for both sender and receiver + +Pending "Add a market via admin action, then accrue COMP" +--verify all COMP speeds are correct and new comp index has been added +--mint(), accrue comp for new market + +Pending "Remove a market via admin action, then accrue and transfer previously earned COMP" +--verify all COMP speeds are correct and new comp index has been added, mint(), accrue comp for recently dropped market + +Pending "Accrue COMP in multiple markets simultaneously" +--try one borrow and one supply + +Pending "Governance Update COMP Speed" + +Pending "Comptroller updgrade to change COMP speed" + +Pending "Replenish COMP in Comptroller" + +Pending "Changing COMP rate continues to distribute at the correct speed" + diff --git a/spec/scenario/PriceOracleProxy.scen b/spec/scenario/PriceOracleProxy.scen index ca23b4ba9..42fd5551d 100644 --- a/spec/scenario/PriceOracleProxy.scen +++ b/spec/scenario/PriceOracleProxy.scen @@ -6,10 +6,14 @@ Macro SetupPriceOracleProxy Unitroller SetPendingImpl ScenComptrollerG1 PriceOracleProxy Deploy Admin (PriceOracle Address) (Address Zero) (Address Zero) (Address Zero) (Address Zero) (Address Zero) ComptrollerImpl ScenComptrollerG1 BecomeG1 (PriceOracleProxy Address) 0.1 20 + -- Update to G2 + ComptrollerImpl Deploy StandardG2 ComptrollerG2 + Unitroller SetPendingImpl ComptrollerG2 + ComptrollerImpl ComptrollerG2 BecomeG2 -- Update to G* ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ScenComptroller Become 1e18 [] NewEtherToken cETH NewCToken USDC cUSDC NewCToken SAI cSAI diff --git a/spec/scenario/Timelock.scen b/spec/scenario/Timelock.scen index 820bce370..9d0af373b 100644 --- a/spec/scenario/Timelock.scen +++ b/spec/scenario/Timelock.scen @@ -183,7 +183,7 @@ Test "Reduce reserves for CEther from Timelock and send reserves to external add Trx GasPrice 0 (From Jared (Timelock ExecuteTransaction Jared 1000000000000000000 604900 "" "")) Assert Equal (EtherBalance (Timelock Address)) (Exactly 0e18) -Test "Set Pending Comptroller implemention on Unitroller from Timelock" +Test "Set Pending Comptroller implementation on Unitroller from Timelock" Unitroller Deploy PriceOracle Deploy Simple ComptrollerImpl Deploy ScenarioG1 ScenComptrollerG1 @@ -201,11 +201,11 @@ Test "Set Pending Comptroller implemention on Unitroller from Timelock" Assert Equal (Unitroller PendingAdmin) (Timelock Address) Assert Equal (Unitroller PendingImplementation) (ComptrollerImpl ScenComptroller Address) From Coburn (Timelock QueueTransaction (Unitroller Address) 0 604900 "_acceptAdmin()" "") - From Coburn (Timelock QueueTransaction (ComptrollerImpl ScenComptroller Address) 0 604900 "_become(address)" (Unitroller Address)) + From Coburn (Timelock QueueTransaction (ComptrollerImpl ScenComptroller Address) 0 604900 "_become(address,uint256,address[])" (Unitroller Address) "1000000000000000000" []) FreezeTime 604900 From Coburn (Timelock ExecuteTransaction (Unitroller Address) 0 604900 "_acceptAdmin()" "") Assert Equal (Unitroller Admin) (Timelock Address) Assert Equal (Unitroller PendingAdmin) (Address Zero) - From Coburn (Timelock ExecuteTransaction (ComptrollerImpl ScenComptroller Address) 0 604900 "_become(address)" (Unitroller Address)) + From Coburn (Timelock ExecuteTransaction (ComptrollerImpl ScenComptroller Address) 0 604900 "_become(address,uint256,address[])" (Unitroller Address) "1000000000000000000" []) Assert Equal (Unitroller Implementation) (Address ScenComptroller) Assert Equal (Unitroller PendingImplementation) (Address Zero) diff --git a/spec/scenario/Unitroller.scen b/spec/scenario/Unitroller.scen index 7af9b5c18..c5a307ce1 100644 --- a/spec/scenario/Unitroller.scen +++ b/spec/scenario/Unitroller.scen @@ -9,10 +9,19 @@ Test "Standard Upgrade" Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1) - -- Upgrade to G* + ListedCToken ZRX cZRX + ListedCToken DAI cDAI + -- Upgrade to G2 + ComptrollerImpl Deploy StandardG2 StandardComptrollerG2 + Unitroller SetPendingImpl StandardComptrollerG2 + ComptrollerImpl StandardComptrollerG2 BecomeG2 + Assert Equal (Comptroller CloseFactor) 0.2 + Assert Equal (Comptroller MaxAssets) 20 + Assert Equal (Comptroller Implementation) (Address StandardComptrollerG2) + -- Upgrade to G3 ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ScenComptroller Become 1e18 [cZRX cDAI] Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 Assert Equal (Comptroller Implementation) (Address ScenComptroller) @@ -27,25 +36,35 @@ Test "Standard Upgrade, then downgrade then upgrade again" Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1) - -- Upgrade to G* + ListedCToken ZRX cZRX + ListedCToken DAI cDAI + -- Upgrade to G2 + ComptrollerImpl Deploy StandardG2 ComptrollerG2 + Unitroller SetPendingImpl ComptrollerG2 + ComptrollerImpl ComptrollerG2 BecomeG2 + Comptroller SetPauseGuardian Coburn + Assert Equal (Comptroller PauseGuardian) (Address Coburn) + Assert Equal (Comptroller CloseFactor) 0.2 + Assert Equal (Comptroller MaxAssets) 20 + Assert Equal (Comptroller Implementation) (Address ComptrollerG2) + -- Upgrade to G3 ComptrollerImpl Deploy Scenario ScenComptroller Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become - Comptroller SetPauseGuardian Coburn + ComptrollerImpl ScenComptroller Become 1e18 [cZRX cDAI] Assert Equal (Comptroller PauseGuardian) (Address Coburn) Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 Assert Equal (Comptroller Implementation) (Address ScenComptroller) - -- Downgrade to G1 - Unitroller SetPendingImpl ScenComptrollerG1 - ComptrollerImpl ScenComptrollerG1 Recome - Assert ReadRevert (Comptroller PauseGuardian) "revert" + -- Downgrade to G2 + Unitroller SetPendingImpl ComptrollerG2 + ComptrollerImpl ComptrollerG2 BecomeG2 Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 - Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1) - -- Upgrade again + Assert Equal (Comptroller Implementation) (Address ComptrollerG2) + -- Upgrade to G3 again Unitroller SetPendingImpl ScenComptroller - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ScenComptroller Become 1e18 [] + Assert Equal (Comptroller GetCompMarkets) ([(Address cZRX) (Address cDAI)]) Assert Equal (Comptroller PauseGuardian) (Address Coburn) Assert Equal (Comptroller CloseFactor) 0.2 Assert Equal (Comptroller MaxAssets) 20 @@ -68,6 +87,7 @@ Pending "Once become, can become again" Assert Equal (Comptroller MaxAssets) 40 Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1_2) +--G1 recome Test "Recome has default values" Unitroller Deploy PriceOracle Deploy Fixed 1.0 @@ -78,6 +98,7 @@ Test "Recome has default values" Assert Equal (Comptroller MaxAssets) 0 Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1) +--G1 bork Test "Bork and unbork" Unitroller Deploy PriceOracle Deploy Fixed 1.0 @@ -101,6 +122,7 @@ Test "Bork and unbork" Assert Equal (Comptroller MaxAssets) 20 Assert Equal (Comptroller Implementation) (Address ScenComptrollerG1) +--Just tests G1, G2 Test "Keeps all storage" Unitroller Deploy PriceOracle Deploy Fixed 1.0 @@ -131,10 +153,10 @@ Test "Keeps all storage" Assert Equal (Unitroller Implementation) (Address ScenComptrollerG1) Assert Equal (StorageAt Comptroller 2 0 "address") (Address ScenComptrollerG1) -- PendingComptrollerImplementation; 3 - ComptrollerImpl Deploy Scenario ScenComptroller - Unitroller SetPendingImpl ScenComptroller - Assert Equal (Unitroller PendingImplementation) (Address ScenComptroller) - Assert Equal (StorageAt Comptroller 3 0 "address") (Address ScenComptroller) + ComptrollerImpl Deploy StandardG2 ComptrollerG2 + Unitroller SetPendingImpl ComptrollerG2 + Assert Equal (Unitroller PendingImplementation) (Address ComptrollerG2) + Assert Equal (StorageAt Comptroller 3 0 "address") (Address ComptrollerG2) -- -- V1 Storage -- @@ -166,7 +188,7 @@ Test "Keeps all storage" Assert Equal (Comptroller CheckMembership Geoff cZRX) True Assert Equal (Comptroller CheckMembership Geoff cBAT) True -- - ComptrollerImpl ScenComptroller Become + ComptrollerImpl ComptrollerG2 BecomeG2 -- -- Recheck all unitroller and v1 storage -- @@ -177,8 +199,8 @@ Test "Keeps all storage" -- PendingAdmin; 1 Assert Equal (StorageAt Comptroller 1 0 "address") (Address Coburn) -- ComptrollerImplementation; 2 - Assert Equal (Unitroller Implementation) (Address ScenComptroller) - Assert Equal (StorageAt Comptroller 2 0 "address") (Address ScenComptroller) + Assert Equal (Unitroller Implementation) (Address ComptrollerG2) + Assert Equal (StorageAt Comptroller 2 0 "address") (Address ComptrollerG2) -- PendingComptrollerImplementation; 3 -- check as number since casting address 0 is not defined Assert Equal (StorageAt Comptroller 3 0 "number") 0 diff --git a/tests/Contracts/ComptrollerHarness.sol b/tests/Contracts/ComptrollerHarness.sol index 45753fa5b..2a10dc735 100644 --- a/tests/Contracts/ComptrollerHarness.sol +++ b/tests/Contracts/ComptrollerHarness.sol @@ -4,11 +4,81 @@ import "../../contracts/Comptroller.sol"; import "../../contracts/PriceOracle.sol"; contract ComptrollerHarness is Comptroller { + address compAddress; + uint public blockNumber; + constructor() Comptroller() public {} function setPauseGuardian(address harnessedPauseGuardian) public { pauseGuardian = harnessedPauseGuardian; } + + function setCompSupplyState(address cToken, uint224 index, uint32 blockNumber_) public { + compSupplyState[cToken].index = index; + compSupplyState[cToken].block = blockNumber_; + } + + function setCompBorrowState(address cToken, uint224 index, uint32 blockNumber_) public { + compBorrowState[cToken].index = index; + compBorrowState[cToken].block = blockNumber_; + } + + function setCompAccrued(address user, uint userAccrued) public { + compAccrued[user] = userAccrued; + } + + function setCompAddress(address compAddress_) public { + compAddress = compAddress_; + } + + function getCompAddress() public view returns (address) { + return compAddress; + } + + function setCompSpeed(address cToken, uint compSpeed) public { + compSpeeds[cToken] = compSpeed; + } + + function setCompBorrowerIndex(address cToken, address borrower, uint index) public { + compBorrowerIndex[cToken][borrower] = index; + } + + function setCompSupplierIndex(address cToken, address supplier, uint index) public { + compSupplierIndex[cToken][supplier] = index; + } + + function harnessUpdateCompBorrowIndex(address cToken, uint marketBorrowIndexMantissa) public { + updateCompBorrowIndex(cToken, Exp({mantissa: marketBorrowIndexMantissa})); + } + + function harnessUpdateCompSupplyIndex(address cToken) public { + updateCompSupplyIndex(cToken); + } + + function harnessDistributeBorrowerComp(address cToken, address borrower, uint marketBorrowIndexMantissa) public { + distributeBorrowerComp(cToken, borrower, Exp({mantissa: marketBorrowIndexMantissa})); + } + + function harnessDistributeSupplierComp(address cToken, address supplier) public { + distributeSupplierComp(cToken, supplier); + } + + function harnessTransferComp(address user, uint userAccrued, uint threshold) public returns (uint) { + return transferComp(user, userAccrued, threshold); + } + + function harnessFastForward(uint blocks) public returns (uint) { + blockNumber += blocks; + return blockNumber; + } + + function setBlockNumber(uint number) public { + blockNumber = number; + } + + function getBlockNumber() public view returns (uint) { + return blockNumber; + } } contract ComptrollerScenario is Comptroller { @@ -30,8 +100,8 @@ contract ComptrollerScenario is Comptroller { blockNumber = number; } - function _become(Unitroller unitroller) public { - super._become(unitroller); + function getBlockNumber() public view returns (uint) { + return blockNumber; } function unlist(CToken cToken) public { diff --git a/tests/Flywheel/FlywheelTest.js b/tests/Flywheel/FlywheelTest.js new file mode 100644 index 000000000..8856c084f --- /dev/null +++ b/tests/Flywheel/FlywheelTest.js @@ -0,0 +1,582 @@ +const { + makeComptroller, + makeCToken, + balanceOf, + fastForward, + pretendBorrow, + quickMint +} = require('../Utils/Compound'); +const { + etherExp, + etherDouble, + etherUnsigned +} = require('../Utils/Ethereum'); + +const compRate = etherUnsigned(1e18); + +async function compAccrued(comptroller, user) { + return etherUnsigned(await call(comptroller, 'compAccrued', [user])); +} + +async function compBalance(comptroller, user) { + return etherUnsigned(await call(comptroller.comp, 'balanceOf', [user])) +} + +async function totalCompAccrued(comptroller, user) { + return (await compAccrued(comptroller, user)).add(await compBalance(comptroller, user)); +} + +describe('Flywheel upgrade', () => { + describe('becomes the comptroller', () => { + it('adds the comp markets', async () => { + let root = saddle.accounts[0]; + let unitroller = await makeComptroller({kind: 'unitroller-prior'}); + let compMarkets = await Promise.all([1, 2, 3].map(async _ => { + return makeCToken({comptroller: unitroller, supportMarket: true}); + })); + compMarkets = compMarkets.map(c => c._address); + unitroller = await makeComptroller({kind: 'unitroller', unitroller, compMarkets}); + expect(await call(unitroller, 'getCompMarkets')).toEqual(compMarkets); + }); + }); +}); + +describe('Flywheel', () => { + let root, a1, a2, a3, accounts; + let comptroller, cLOW, cREP, cZRX; + beforeEach(async () => { + let interestRateModelOpts = {borrowRate: 0.000001}; + [root, a1, a2, a3, ...accounts] = saddle.accounts; + comptroller = await makeComptroller(); + cLOW = await makeCToken({comptroller, supportMarket: true, underlyingPrice: 1, interestRateModelOpts}); + cREP = await makeCToken({comptroller, supportMarket: true, underlyingPrice: 2, interestRateModelOpts}); + cZRX = await makeCToken({comptroller, supportMarket: true, underlyingPrice: 3, interestRateModelOpts}); + await send(comptroller, '_addCompMarkets', [[cLOW, cREP, cZRX].map(c => c._address)]); + }); + + describe('getCompMarkets()', () => { + it('should return the comp markets', async () => { + expect(await call(comptroller, 'getCompMarkets')).toEqual( + [cLOW, cREP, cZRX].map((c) => c._address) + ); + }); + }); + + describe('updateCompBorrowIndex()', () => { + it('should calculate comp borrower index correctly', async () => { + const mkt = cREP; + await send(comptroller, 'setBlockNumber', [100]); + await send(mkt, 'harnessSetTotalBorrows', [etherUnsigned(11e18)]); + await send(comptroller, 'setCompSpeed', [mkt._address, etherExp(0.5)]); + await send(comptroller, 'harnessUpdateCompBorrowIndex', [ + mkt._address, + etherExp(1.1), + ]); + /* + 100 blocks, 10e18 origin total borrows, 0.5e18 borrowSpeed + + borrowAmt = totalBorrows * 1e18 / borrowIdx + = 11e18 * 1e18 / 1.1e18 = 10e18 + compAccrued = deltaBlocks * borrowSpeed + = 100 * 0.5e18 = 50e18 + newIndex += 1e36 + compAccrued * 1e36 / borrowAmt + = 1e36 + 50e18 * 1e36 / 10e18 = 6e36 + */ + + const {index, block} = await call(comptroller, 'compBorrowState', [mkt._address]); + expect(index).toEqualNumber(6e36); + expect(block).toEqualNumber(100); + }); + + it('should not revert or update compBorrowState index if cToken not in COMP markets', async () => { + const mkt = await makeCToken({ + comptroller: comptroller, + supportMarket: true, + addCompMarket: false, + }); + await send(comptroller, 'setBlockNumber', [100]); + await send(comptroller, 'harnessUpdateCompBorrowIndex', [ + mkt._address, + etherExp(1.1), + ]); + + const {index, block} = await call(comptroller, 'compBorrowState', [mkt._address]); + expect(index).toEqualNumber(0); + expect(block).toEqualNumber(100); + const speed = await call(comptroller, 'compSpeeds', [mkt._address]); + expect(speed).toEqualNumber(0); + }); + + it('should not update index if no blocks passed since last accrual', async () => { + const mkt = cREP; + await send(comptroller, 'setCompSpeed', [mkt._address, etherExp(0.5)]); + await send(comptroller, 'harnessUpdateCompBorrowIndex', [ + mkt._address, + etherExp(1.1), + ]); + + const {index, block} = await call(comptroller, 'compBorrowState', [mkt._address]); + expect(index).toEqualNumber(1e36); + expect(block).toEqualNumber(0); + }); + + it('should not update index if comp speed is 0', async () => { + const mkt = cREP; + await send(comptroller, 'setCompSpeed', [mkt._address, etherExp(0)]); + await send(comptroller, 'setBlockNumber', [100]); + await send(comptroller, 'harnessUpdateCompBorrowIndex', [ + mkt._address, + etherExp(1.1), + ]); + + const {index, block} = await call(comptroller, 'compBorrowState', [mkt._address]); + expect(index).toEqualNumber(1e36); + expect(block).toEqualNumber(100); + }); + }); + + describe('updateCompSupplyIndex()', () => { + it('should calculate comp supplier index correctly', async () => { + const mkt = cREP; + await send(comptroller, 'setBlockNumber', [100]); + await send(mkt, 'harnessSetTotalSupply', [etherUnsigned(10e18)]); + await send(comptroller, 'setCompSpeed', [mkt._address, etherExp(0.5)]); + await send(comptroller, 'harnessUpdateCompSupplyIndex', [mkt._address]); + /* + suppyTokens = 10e18 + compAccrued = deltaBlocks * supplySpeed + = 100 * 0.5e18 = 50e18 + newIndex += compAccrued * 1e36 / supplyTokens + = 1e36 + 50e18 * 1e36 / 10e18 = 6e36 + */ + const {index, block} = await call(comptroller, 'compSupplyState', [mkt._address]); + expect(index).toEqualNumber(6e36); + expect(block).toEqualNumber(100); + }); + + it('should not update index on non-COMP markets', async () => { + const mkt = await makeCToken({ + comptroller: comptroller, + supportMarket: true, + addCompMarket: false + }); + await send(comptroller, 'setBlockNumber', [100]); + await send(comptroller, 'harnessUpdateCompSupplyIndex', [ + mkt._address + ]); + + const {index, block} = await call(comptroller, 'compSupplyState', [mkt._address]); + expect(index).toEqualNumber(0); + expect(block).toEqualNumber(100); + const speed = await call(comptroller, 'compSpeeds', [mkt._address]); + expect(speed).toEqualNumber(0); + // ctoken could have no comp speed or comp supplier state if not in comp markets + // this logic could also possibly be implemented in the allowed hook + }); + + it('should not update index if no blocks passed since last accrual', async () => { + const mkt = cREP; + await send(comptroller, 'setBlockNumber', [0]); + await send(mkt, 'harnessSetTotalSupply', [etherUnsigned(10e18)]); + await send(comptroller, 'setCompSpeed', [mkt._address, etherExp(0.5)]); + await send(comptroller, 'harnessUpdateCompSupplyIndex', [mkt._address]); + + const {index, block} = await call(comptroller, 'compSupplyState', [mkt._address]); + expect(index).toEqualNumber(1e36); + expect(block).toEqualNumber(0); + }); + + it('should not matter if the index is updated multiple times', async () => { + const compRemaining = compRate.mul(100) + await send(comptroller.comp, 'transfer', [comptroller._address, compRemaining], {from: root}); + await pretendBorrow(cLOW, a1, 1, 1, 100); + await send(comptroller, 'refreshCompSpeeds'); + + await quickMint(cLOW, a2, etherUnsigned(10e18)); + await quickMint(cLOW, a3, etherUnsigned(15e18)); + + const a2Accrued0 = await totalCompAccrued(comptroller, a2); + const a3Accrued0 = await totalCompAccrued(comptroller, a3); + const a2Balance0 = await balanceOf(cLOW, a2); + const a3Balance0 = await balanceOf(cLOW, a3); + + await fastForward(comptroller, 20); + + const txT1 = await send(cLOW, 'transfer', [a2, a3Balance0.sub(a2Balance0)], {from: a3}); + + const a2Accrued1 = await totalCompAccrued(comptroller, a2); + const a3Accrued1 = await totalCompAccrued(comptroller, a3); + const a2Balance1 = await balanceOf(cLOW, a2); + const a3Balance1 = await balanceOf(cLOW, a3); + + await fastForward(comptroller, 10); + await send(comptroller, 'harnessUpdateCompSupplyIndex', [cLOW._address]); + await fastForward(comptroller, 10); + + const txT2 = await send(cLOW, 'transfer', [a3, a2Balance1.sub(a3Balance1)], {from: a2}); + + const a2Accrued2 = await totalCompAccrued(comptroller, a2); + const a3Accrued2 = await totalCompAccrued(comptroller, a3); + + expect(a2Accrued0).toEqualNumber(0); + expect(a3Accrued0).toEqualNumber(0); + expect(a2Accrued1).not.toEqualNumber(0); + expect(a3Accrued1).not.toEqualNumber(0); + expect(a2Accrued1).toEqualNumber(a3Accrued2.sub(a3Accrued1)); + expect(a3Accrued1).toEqualNumber(a2Accrued2.sub(a2Accrued1)); + + expect(txT1.gasUsed).toBeLessThan(200000); + expect(txT1.gasUsed).toBeGreaterThan(150000); + expect(txT2.gasUsed).toBeLessThan(200000); + expect(txT2.gasUsed).toBeGreaterThan(150000); + }); + }); + + describe('distributeBorrowerComp()', () => { + + it('should update borrow index checkpoint but not compAccrued for first time user', async () => { + const mkt = cREP; + await send(comptroller, "setCompBorrowState", [mkt._address, etherDouble(6), 10]); + await send(comptroller, "setCompBorrowerIndex", [mkt._address, root, etherUnsigned(0)]); + + await send(comptroller, "harnessDistributeBorrowerComp", [mkt._address, root, etherExp(1.1)]); + expect(await call(comptroller, "compAccrued", [root])).toEqualNumber(0); + expect(await call(comptroller, "compBorrowerIndex", [ mkt._address, root])).toEqualNumber(6e36); + }); + + it('should transfer comp and update borrow index checkpoint correctly for repeat time user', async () => { + const mkt = cREP; + await send(comptroller.comp, 'transfer', [comptroller._address, etherUnsigned(50e18)], {from: root}); + await send(mkt, "harnessSetAccountBorrows", [a1, etherUnsigned(5.5e18), etherExp(1)]); + await send(comptroller, "setCompBorrowState", [mkt._address, etherDouble(6), 10]); + await send(comptroller, "setCompBorrowerIndex", [mkt._address, a1, etherDouble(1)]); + + /* + * 100 delta blocks, 10e18 origin total borrows, 0.5e18 borrowSpeed => 6e18 compBorrowIndex + * this tests that an acct with half the total borrows over that time gets 25e18 COMP + borrowerAmount = borrowBalance * 1e18 / borrow idx + = 5.5e18 * 1e18 / 1.1e18 = 5e18 + deltaIndex = marketStoredIndex - userStoredIndex + = 6e36 - 1e36 = 5e36 + borrowerAccrued= borrowerAmount * deltaIndex / 1e36 + = 5e18 * 5e36 / 1e36 = 25e18 + */ + const tx = await send(comptroller, "harnessDistributeBorrowerComp", [mkt._address, a1, etherUnsigned(1.1e18)]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0); + expect(await compBalance(comptroller, a1)).toEqualNumber(25e18); + expect(tx).toHaveLog('DistributedBorrowerComp', { + cToken: mkt._address, + borrower: a1, + compDelta: etherUnsigned(25e18).toString(), + compBorrowIndex: etherDouble(6).toString() + }); + }); + + it('should not transfer if below comp claim threshold', async () => { + const mkt = cREP; + await send(comptroller.comp, 'transfer', [comptroller._address, etherUnsigned(50e18)], {from: root}); + await send(mkt, "harnessSetAccountBorrows", [a1, etherUnsigned(5.5e18), etherExp(1)]); + await send(comptroller, "setCompBorrowState", [mkt._address, etherDouble(1.0019), 10]); + await send(comptroller, "setCompBorrowerIndex", [mkt._address, a1, etherDouble(1)]); + /* + borrowerAmount = borrowBalance * 1e18 / borrow idx + = 5.5e18 * 1e18 / 1.1e18 = 5e18 + deltaIndex = marketStoredIndex - userStoredIndex + = 1.0019e36 - 1e36 = 0.0019e36 + borrowerAccrued= borrowerAmount * deltaIndex / 1e36 + = 5e18 * 0.0019e36 / 1e36 = 0.0095e18 + 0.0095e18 < compClaimThreshold of 0.01e18 + */ + await send(comptroller, "harnessDistributeBorrowerComp", [mkt._address, a1, etherExp(1.1)]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0.0095e18); + expect(await compBalance(comptroller, a1)).toEqualNumber(0); + }); + + it('should not revert or distribute when called with non-COMP market', async () => { + const mkt = await makeCToken({ + comptroller: comptroller, + supportMarket: true, + addCompMarket: false, + }); + + await send(comptroller, "harnessDistributeBorrowerComp", [mkt._address, a1, etherExp(1.1)]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0); + expect(await compBalance(comptroller, a1)).toEqualNumber(0); + expect(await call(comptroller, 'compBorrowerIndex', [mkt._address, a1])).toEqualNumber(0); + }); + }); + + describe('distributeSupplierComp()', () => { + it('should transfer comp and update supply index correctly for first time user', async () => { + const mkt = cREP; + await send(comptroller.comp, 'transfer', [comptroller._address, etherUnsigned(50e18)], {from: root}); + + await send(mkt, "harnessSetBalance", [a1, etherUnsigned(5e18)]); + await send(comptroller, "setCompSupplyState", [mkt._address, etherDouble(6), 10]); + /* + * 100 delta blocks, 10e18 total supply, 0.5e18 supplySpeed => 6e18 compSupplyIndex + * confirming an acct with half the total supply over that time gets 25e18 COMP: + supplierAmount = 5e18 + deltaIndex = marketStoredIndex - userStoredIndex + = 6e36 - 1e36 = 5e36 + suppliedAccrued+= supplierTokens * deltaIndex / 1e36 + = 5e18 * 5e36 / 1e36 = 25e18 + */ + + const tx = await send(comptroller, "harnessDistributeSupplierComp", [mkt._address, a1]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0); + expect(await compBalance(comptroller, a1)).toEqualNumber(25e18); + expect(tx).toHaveLog('DistributedSupplierComp', { + cToken: mkt._address, + supplier: a1, + compDelta: etherUnsigned(25e18).toString(), + compSupplyIndex: etherDouble(6).toString() + }); + }); + + it('should update comp accrued and supply index for repeat user', async () => { + const mkt = cREP; + await send(comptroller.comp, 'transfer', [comptroller._address, etherUnsigned(50e18)], {from: root}); + + await send(mkt, "harnessSetBalance", [a1, etherUnsigned(5e18)]); + await send(comptroller, "setCompSupplyState", [mkt._address, etherDouble(6), 10]); + await send(comptroller, "setCompSupplierIndex", [mkt._address, a1, etherDouble(2)]) + /* + supplierAmount = 5e18 + deltaIndex = marketStoredIndex - userStoredIndex + = 6e36 - 2e36 = 4e36 + suppliedAccrued+= supplierTokens * deltaIndex / 1e36 + = 5e18 * 4e36 / 1e36 = 20e18 + */ + + await send(comptroller, "harnessDistributeSupplierComp", [mkt._address, a1]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0); + expect(await compBalance(comptroller, a1)).toEqualNumber(20e18); + }); + + it('should not transfer when compAccrued below threshold', async () => { + const mkt = cREP; + await send(comptroller.comp, 'transfer', [comptroller._address, etherUnsigned(50e18)], {from: root}); + + await send(mkt, "harnessSetBalance", [a1, etherUnsigned(5e18)]); + await send(comptroller, "setCompSupplyState", [mkt._address, etherDouble(1.0019), 10]); + /* + supplierAmount = 5e18 + deltaIndex = marketStoredIndex - userStoredIndex + = 1.0019e36 - 1e36 = 0.0019e36 + suppliedAccrued+= supplierTokens * deltaIndex / 1e36 + = 5e18 * 0.0019e36 / 1e36 = 0.0095e18 + */ + + await send(comptroller, "harnessDistributeSupplierComp", [mkt._address, a1]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0.0095e18); + expect(await compBalance(comptroller, a1)).toEqualNumber(0); + }); + + it('should not revert or distribute when called with non-COMP market', async () => { + const mkt = await makeCToken({ + comptroller: comptroller, + supportMarket: true, + addCompMarket: false, + }); + + await send(comptroller, "harnessDistributeSupplierComp", [mkt._address, a1]); + expect(await compAccrued(comptroller, a1)).toEqualNumber(0); + expect(await compBalance(comptroller, a1)).toEqualNumber(0); + expect(await call(comptroller, 'compBorrowerIndex', [mkt._address, a1])).toEqualNumber(0); + }); + + }); + + describe('transferComp', () => { + it('should transfer comp accrued when amount is above threshold', async () => { + const compRemaining = 1000, a1AccruedPre = 100, threshold = 1; + const compBalancePre = await compBalance(comptroller, a1); + const tx0 = await send(comptroller.comp, 'transfer', [comptroller._address, compRemaining], {from: root}); + const tx1 = await send(comptroller, 'setCompAccrued', [a1, a1AccruedPre]); + const tx2 = await send(comptroller, 'harnessTransferComp', [a1, a1AccruedPre, threshold]); + const a1AccruedPost = await compAccrued(comptroller, a1); + const compBalancePost = await compBalance(comptroller, a1); + expect(compBalancePre).toEqualNumber(0); + expect(compBalancePost).toEqualNumber(a1AccruedPre); + }); + + it('should not transfer when comp accrued is below threshold', async () => { + const compRemaining = 1000, a1AccruedPre = 100, threshold = 101; + const compBalancePre = await call(comptroller.comp, 'balanceOf', [a1]); + const tx0 = await send(comptroller.comp, 'transfer', [comptroller._address, compRemaining], {from: root}); + const tx1 = await send(comptroller, 'setCompAccrued', [a1, a1AccruedPre]); + const tx2 = await send(comptroller, 'harnessTransferComp', [a1, a1AccruedPre, threshold]); + const a1AccruedPost = await compAccrued(comptroller, a1); + const compBalancePost = await compBalance(comptroller, a1); + expect(compBalancePre).toEqualNumber(0); + expect(compBalancePost).toEqualNumber(0); + }); + + it('should not transfer comp if comp accrued is greater than comp remaining', async () => { + const compRemaining = 99, a1AccruedPre = 100, threshold = 1; + const compBalancePre = await compBalance(comptroller, a1); + const tx0 = await send(comptroller.comp, 'transfer', [comptroller._address, compRemaining], {from: root}); + const tx1 = await send(comptroller, 'setCompAccrued', [a1, a1AccruedPre]); + const tx2 = await send(comptroller, 'harnessTransferComp', [a1, a1AccruedPre, threshold]); + const a1AccruedPost = await compAccrued(comptroller, a1); + const compBalancePost = await compBalance(comptroller, a1); + expect(compBalancePre).toEqualNumber(0); + expect(compBalancePost).toEqualNumber(0); + }); + }); + + describe('claimComp', () => { + it('should accrue comp and then transfer comp accrued', async () => { + const compRemaining = compRate.mul(100), mintAmount = etherUnsigned(12e18), deltaBlocks = 10; + await send(comptroller.comp, 'transfer', [comptroller._address, compRemaining], {from: root}); + await pretendBorrow(cLOW, a1, 1, 1, 100); + await send(comptroller, 'refreshCompSpeeds'); + const speed = await call(comptroller, 'compSpeeds', [cLOW._address]); + const a2AccruedPre = await compAccrued(comptroller, a2); + const compBalancePre = await compBalance(comptroller, a2); + await quickMint(cLOW, a2, mintAmount); + await fastForward(comptroller, deltaBlocks); + await send(comptroller, 'claimComp', [a2]); + const a2AccruedPost = await compAccrued(comptroller, a2); + const compBalancePost = await compBalance(comptroller, a2); + expect(speed).toEqualNumber(compRate); + expect(a2AccruedPre).toEqualNumber(0); + expect(a2AccruedPost).toEqualNumber(0); + expect(compBalancePre).toEqualNumber(0); + expect(compBalancePost).toEqualNumber(compRate.mul(deltaBlocks).sub(1)); // index is 8333... + }); + }); + + describe('refreshCompSpeeds', () => { + it('should start out 0', async () => { + await send(comptroller, 'refreshCompSpeeds'); + const speed = await call(comptroller, 'compSpeeds', [cLOW._address]); + expect(speed).toEqualNumber(0); + }); + + it('should get correct speeds with borrows', async () => { + await pretendBorrow(cLOW, a1, 1, 1, 100); + const tx = await send(comptroller, 'refreshCompSpeeds'); + const speed = await call(comptroller, 'compSpeeds', [cLOW._address]); + expect(speed).toEqualNumber(compRate); + expect(tx).toHaveLog(['CompSpeedUpdated', 0], { + cToken: cLOW._address, + newSpeed: speed + }); + expect(tx).toHaveLog(['CompSpeedUpdated', 1], { + cToken: cREP._address, + newSpeed: 0 + }); + expect(tx).toHaveLog(['CompSpeedUpdated', 2], { + cToken: cZRX._address, + newSpeed: 0 + }); + }); + + it('should get correct speeds for 2 assets', async () => { + await pretendBorrow(cLOW, a1, 1, 1, 100); + await pretendBorrow(cZRX, a1, 1, 1, 100); + await send(comptroller, 'refreshCompSpeeds'); + const speed1 = await call(comptroller, 'compSpeeds', [cLOW._address]); + const speed2 = await call(comptroller, 'compSpeeds', [cREP._address]); + const speed3 = await call(comptroller, 'compSpeeds', [cZRX._address]); + expect(speed1).toEqualNumber(compRate.div(4)); + expect(speed2).toEqualNumber(0); + expect(speed3).toEqualNumber(compRate.div(4).mul(3)); + }); +}); + + describe('_addCompMarkets', () => { + it('should correctly add a comp market if called by admin', async () => { + const cBAT = await makeCToken({comptroller, supportMarket: true}); + await send(comptroller, '_addCompMarkets', [[cBAT._address]]); + const markets = await call(comptroller, 'getCompMarkets'); + expect(markets).toEqual([cLOW, cREP, cZRX, cBAT].map((c) => c._address)); + }); + + it('should revert if not called by admin', async () => { + const cBAT = await makeCToken({ comptroller, supportMarket: true }); + await expect( + send(comptroller, '_addCompMarkets', [[cBAT._address]], {from: a1}) + ).rejects.toRevert('revert only admin can add comp market'); + }); + + it('should not add non-listed markets', async () => { + const cBAT = await makeCToken({ comptroller, supportMarket: false }); + await expect( + send(comptroller, '_addCompMarkets', [[cBAT._address]]) + ).rejects.toRevert('revert comp market is not listed'); + + const markets = await call(comptroller, 'getCompMarkets'); + expect(markets).toEqual([cLOW, cREP, cZRX].map((c) => c._address)); + }); + + it('should not add duplicate markets', async () => { + const cBAT = await makeCToken({comptroller, supportMarket: true}); + await send(comptroller, '_addCompMarkets', [[cBAT._address]]); + + await expect( + send(comptroller, '_addCompMarkets', [[cBAT._address]]) + ).rejects.toRevert('revert comp market already added'); + }); + + it('should not write over a markets existing state', async () => { + const mkt = cLOW._address; + const bn0 = 10, bn1 = 20; + const idx = etherUnsigned(1.5e36); + + await send(comptroller, "setCompSupplyState", [mkt, idx, bn0]); + await send(comptroller, "setCompBorrowState", [mkt, idx, bn0]); + await send(comptroller, "_dropCompMarket", [mkt]); + await send(comptroller, "setBlockNumber", [bn1]); + await send(comptroller, "_addCompMarkets", [[mkt]]); + + const supplyState = await call(comptroller, 'compSupplyState', [mkt]); + expect(supplyState.block).toEqual(bn1.toString()); + expect(supplyState.index).toEqual(idx.toString()); + + const borrowState = await call(comptroller, 'compBorrowState', [mkt]); + expect(borrowState.block).toEqual(bn1.toString()); + expect(borrowState.index).toEqual(idx.toString()); + }); + }); + + describe('_dropCompMarket', () => { + it('should correctly drop a comp market if called by admin', async () => { + await send(comptroller, '_dropCompMarket', [cLOW._address]); + expect(await call(comptroller, 'getCompMarkets')).toEqual( + [cZRX, cREP].map((c) => c._address) + ); + }); + + it('should correctly drop a comp market from middle of array', async () => { + await send(comptroller, '_dropCompMarket', [cREP._address]); + expect(await call(comptroller, 'getCompMarkets')).toEqual( + [cLOW, cZRX].map((c) => c._address) + ); + }); + + it('should not drop a comp market unless called by admin', async () => { + await expect( + send(comptroller, '_dropCompMarket', [cLOW._address], {from: a1}) + ).rejects.toRevert('revert only admin can drop comp market'); + }); + }); + + describe('_setCompRate', () => { + it('should correctly change comp rate if called by admin', async () => { + expect(await call(comptroller, 'compRate')).toEqualNumber(etherUnsigned(1e18)); + await send(comptroller, '_setCompRate', [etherUnsigned(3e18)]); + expect(await call(comptroller, 'compRate')).toEqualNumber(etherUnsigned(3e18)); + await send(comptroller, '_setCompRate', [etherUnsigned(2e18)]); + expect(await call(comptroller, 'compRate')).toEqualNumber(etherUnsigned(2e18)); + }); + + it('should not change comp rate unless called by admin', async () => { + await expect( + send(comptroller, '_setCompRate', [cLOW._address], {from: a1}) + ).rejects.toRevert('revert only admin can change comp rate'); + }); + }); +}); diff --git a/tests/Fuzz/CompWheelFuzzTest.js b/tests/Fuzz/CompWheelFuzzTest.js new file mode 100644 index 000000000..837db67bd --- /dev/null +++ b/tests/Fuzz/CompWheelFuzzTest.js @@ -0,0 +1,699 @@ +let rand = x => new bn(Math.floor(Math.random() * x)); +let range = count => [...Array(count).keys()]; + +const bn = require('bignumber.js'); +bn.config({ ROUNDING_MODE: bn.ROUND_HALF_DOWN }); + +const RUN_COUNT = 20; +const NUM_EVENTS = 50; +const PRECISION_DECIMALS = 15; + +class AssertionError extends Error { + constructor(assertion, reason, event, index) { + let message = `Assertion Error: ${reason} when processing ${JSON.stringify( + event + )} at pos ${index}`; + + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +expect.extend({ + toFuzzPass(assertion, expected, actual, reason, state, events) { + let eventStr = events + .filter(({ action, failed }) => !failed) + .map(event => `${JSON.stringify(event)},`) + .join('\n'); + + return { + pass: !!assertion(expected, actual), + message: () => ` + Expected: ${JSON.stringify(expected)}, + Actual: ${JSON.stringify(actual)}, + Reason: ${reason} + State: \n${JSON.stringify(state, null, '\t')} + Events:\n${eventStr} + ` + }; + } +}); + +describe.skip('CompWheelFuzzTest', () => { + // This whole test is fake, but we're testing to see if our equations match reality. + + // First, we're going to build a simple simulator of the Compound protocol + + let randAccount = globals => { + return globals.accounts[rand(globals.accounts.length)]; + }; + + let get = src => { + return src || new bn(0); + }; + + let isPositive = (src) => { + assert(bn.isBigNumber(src), "isPositive got wrong type: expected bigNumber"); + return src.decimalPlaces(PRECISION_DECIMALS).isGreaterThan(0); + } + + let almostEqual = (expected, actual) => { + return expected.decimalPlaces(PRECISION_DECIMALS).eq(actual.decimalPlaces(PRECISION_DECIMALS)); + }; + + let deepCopy = src => { + return Object.entries(src).reduce((acc, [key, val]) => { + if (bn.isBigNumber(val)) { + return { + ...acc, + [key]: new bn(val) + }; + } else { + return { + ...acc, + [key]: deepCopy(val) + }; + } + }, {}); + }; + + let initialState = globals => { + return { + // ctoken + accrualBlockNumber: globals.blockNumber, + borrowIndex: new bn(1), + totalCash: new bn(0), + totalSupply: new bn(0), + totalBorrows: new bn(0), + totalReserves: new bn(0), + reserveFactor: new bn(0.05), + + balances: {}, + borrowBalances: {}, + borrowIndexSnapshots: {}, + + // flywheel & comptroller + compSupplySpeed: new bn(1), + compSupplyIndex: new bn(1), + compSupplyIndexSnapshots: {}, + compSupplyIndexUpdatedBlock: globals.blockNumber, + + compBorrowSpeed: new bn(1), + compBorrowIndex: new bn(1), + compBorrowIndexSnapshots: {}, + compSupplyIndexUpdatedBlock: globals.blockNumber, + + compAccruedWithCrank: {}, // naive method, accruing all accounts every block + compAccruedWithIndex: {}, // with indices + + activeBorrowBlocks: new bn(0), // # blocks with an active borrow, for which we expect to see comp distributed. just for fuzz testing. + activeSupplyBlocks: new bn(0) + }; + }; + + let getExchangeRate = ({ + totalCash, + totalSupply, + totalBorrows, + totalReserves + }) => { + if (isPositive(totalSupply)) { + return totalCash + .plus(totalBorrows) + .minus(totalReserves) + .div(totalSupply); + } else { + return new bn(1); + } + }; + + let getBorrowRate = (cash, borrows, reserves) => { + let denom = cash.plus(borrows).minus(reserves); + if (denom.isZero()) { + return new bn(0); + } else if (denom.lt(0)) { + throw new Error( + `Borrow Rate failure cash:${cash} borrows:${borrows} reserves:${reserves}` + ); + } else { + let util = borrows.div(denom); + return util.times(0.001); + } + }; + + // only used after events are run to test invariants + let trueUpComp = (globals, state) => { + state = accrueInterest(globals, state); + + state = Object.keys(state.compSupplyIndexSnapshots).reduce( + (acc, account) => supplierFlywheelByIndex(globals, state, account), + state + ); + + state = Object.keys(state.compBorrowIndexSnapshots).reduce( + (acc, account) => borrowerFlywheelByIndex(globals, state, account), + state + ); + + return state; + }; + + // manual flywheel loops through every account and updates comp accrued mapping + // cranked within accrue interest (borrowBalance not updated, totalBorrows should be) + let flywheelByCrank = ( + state, + deltaBlocks, + borrowIndexNew, + borrowIndexPrev + ) => { + let { + balances, + compBorrowSpeed, + compSupplySpeed, + totalSupply, + totalBorrows, + compAccruedWithCrank, + borrowBalances + } = state; + + // suppliers + for (let [account, balance] of Object.entries(balances)) { + if (isPositive(totalSupply)) { + compAccruedWithCrank[account] = get( + state.compAccruedWithCrank[account] + ).plus( + deltaBlocks + .times(compSupplySpeed) + .times(balance) + .div(totalSupply) + ); + } + } + + // borrowers + for (let [account, borrowBalance] of Object.entries(borrowBalances)) { + if (isPositive(totalBorrows)) { + let truedUpBorrowBalance = getAccruedBorrowBalance(state, account); + + compAccruedWithCrank[account] = get( + state.compAccruedWithCrank[account] + ).plus( + deltaBlocks + .times(compBorrowSpeed) + .times(truedUpBorrowBalance) + .div(totalBorrows) + ); + } + } + + return { + ...state, + compAccruedWithCrank: compAccruedWithCrank, + }; + }; + + // real deal comp index flywheel™️ + let borrowerFlywheelByIndex = (globals, state, account) => { + let { + compBorrowSpeed, + compBorrowIndex, + compBorrowIndexSnapshots, + compAccruedWithIndex, + totalBorrows, + borrowBalances, + compBorrowIndexUpdatedBlock, + borrowIndex, + borrowIndexSnapshots + } = state; + + let deltaBlocks = globals.blockNumber.minus(compBorrowIndexUpdatedBlock); + if (isPositive(totalBorrows)) { + let scaledTotalBorrows = totalBorrows.div(borrowIndex); + compBorrowIndex = compBorrowIndex.plus( + compBorrowSpeed.times(deltaBlocks).div(scaledTotalBorrows) + ); + } + + let indexSnapshot = compBorrowIndexSnapshots[account]; + + if ( + indexSnapshot !== undefined && + compBorrowIndex.isGreaterThan(indexSnapshot) && + borrowBalances[account] !== undefined + ) { + // to simulate borrowBalanceStored + let borrowBalanceNew = borrowBalances[account] + .times(borrowIndex) + .div(state.borrowIndexSnapshots[account]); + compAccruedWithIndex[account] = get(compAccruedWithIndex[account]).plus( + borrowBalanceNew + .div(borrowIndex) + .times(compBorrowIndex.minus(indexSnapshot)) + ); + } + + return { + ...state, + compBorrowIndexUpdatedBlock: globals.blockNumber, + compBorrowIndex: compBorrowIndex, + compBorrowIndexSnapshots: { + ...state.compBorrowIndexSnapshots, + [account]: compBorrowIndex + } + }; + }; + + // real deal comp index flywheel™️ + let supplierFlywheelByIndex = (globals, state, account) => { + let { + balances, + compSupplySpeed, + compSupplyIndex, + compSupplyIndexSnapshots, + compAccruedWithIndex, + totalSupply, + compSupplyIndexUpdatedBlock + } = state; + + let deltaBlocks = globals.blockNumber.minus(compSupplyIndexUpdatedBlock); + + if (isPositive(totalSupply)) { + compSupplyIndex = compSupplyIndex.plus( + compSupplySpeed.times(deltaBlocks).div(totalSupply) + ); + } + + let indexSnapshot = compSupplyIndexSnapshots[account]; + if (indexSnapshot !== undefined) { + // if had prev snapshot, accrue some comp + compAccruedWithIndex[account] = get(compAccruedWithIndex[account]).plus( + balances[account].times(compSupplyIndex.minus(indexSnapshot)) + ); + } + + return { + ...state, + compSupplyIndexUpdatedBlock: globals.blockNumber, + compSupplyIndex: compSupplyIndex, + compSupplyIndexSnapshots: { + ...state.compSupplyIndexSnapshots, + [account]: compSupplyIndex + }, + compAccruedWithIndex: compAccruedWithIndex + }; + }; + + let accrueActiveBlocks = (state, deltaBlocks) => { + let { + activeBorrowBlocks, + activeSupplyBlocks, + totalBorrows, + totalSupply + } = state; + if (isPositive(totalSupply)) { + activeSupplyBlocks = activeSupplyBlocks.plus(deltaBlocks); + } + + if (isPositive(totalBorrows)) { + activeBorrowBlocks = activeBorrowBlocks.plus(deltaBlocks); + } + + return { + ...state, + activeSupplyBlocks: activeSupplyBlocks, + activeBorrowBlocks: activeBorrowBlocks + }; + }; + + let getAccruedBorrowBalance = (state, account) => { + let prevBorrowBalance = state.borrowBalances[account]; + let checkpointBorrowIndex = state.borrowIndexSnapshots[account]; + if ( + prevBorrowBalance !== undefined && + checkpointBorrowIndex !== undefined + ) { + return prevBorrowBalance + .times(state.borrowIndex) + .div(checkpointBorrowIndex); + } else { + return new bn(0); + } + }; + + let accrueInterest = (globals, state) => { + let { + balances, + totalCash, + totalBorrows, + totalSupply, + totalReserves, + accrualBlockNumber, + borrowIndex, + reserveFactor + } = state; + + let deltaBlocks = globals.blockNumber.minus(accrualBlockNumber); + state = accrueActiveBlocks(state, deltaBlocks); + + let borrowRate = getBorrowRate(totalCash, totalBorrows, totalReserves); + let simpleInterestFactor = deltaBlocks.times(borrowRate); + let borrowIndexNew = borrowIndex.times(simpleInterestFactor.plus(1)); + let interestAccumulated = totalBorrows.times(simpleInterestFactor); + let totalBorrowsNew = totalBorrows.plus(interestAccumulated); + let totalReservesNew = totalReserves + .plus(interestAccumulated) + .times(reserveFactor); + + state = flywheelByCrank( + state, + deltaBlocks, + borrowIndexNew, + state.borrowIndex + ); + + return { + ...state, + accrualBlockNumber: globals.blockNumber, + borrowIndex: borrowIndexNew, + totalBorrows: totalBorrowsNew, + totalReserves: totalReservesNew + }; + }; + + let mine = { + action: 'mine', + rate: 10, + run: (globals, state, {}, { assert }) => { + return state; + }, + gen: globals => { + return { + mine: rand(100).plus(1) + }; + } + }; + + let gift = { + action: 'gift', + rate: 3, + run: (globals, state, { amount }, { assert }) => { + amount = new bn(amount); + return { + ...state, + totalCash: state.totalCash.plus(amount) + }; + }, + gen: globals => { + return { + amount: rand(1000) + }; + } + }; + + let test = { + action: 'test', + run: (globals, state, { amount }, { assert }) => { + console.log(state); + return state; + } + }; + + let borrow = { + action: 'borrow', + rate: 10, + run: (globals, state, { account, amount }, { assert }) => { + amount = new bn(amount); + state = accrueInterest(globals, state); + state = borrowerFlywheelByIndex(globals, state, account); + + let newTotalCash = state.totalCash.minus(amount); + assert( + isPositive(newTotalCash.plus(state.totalReserves)), + 'Attempted to borrow more than total cash' + ); + + let newBorrowBalance = getAccruedBorrowBalance(state, account).plus( + amount + ); + assert( + get(state.balances[account]) + .times(getExchangeRate(state)) + .isGreaterThan(newBorrowBalance), + 'Borrower undercollateralized' + ); + + return { + ...state, + totalBorrows: state.totalBorrows.plus(amount), + totalCash: newTotalCash, + borrowBalances: { + ...state.borrowBalances, + [account]: newBorrowBalance + }, + borrowIndexSnapshots: { + ...state.borrowIndexSnapshots, + [account]: state.borrowIndex + } + }; + }, + gen: globals => { + return { + account: randAccount(globals), + amount: rand(1000) + }; + } + }; + + let repayBorrow = { + action: 'repayBorrow', + rate: 10, + run: (globals, state, { account, amount }, { assert }) => { + amount = new bn(amount); + state = accrueInterest(globals, state); + state = borrowerFlywheelByIndex(globals, state, account); + + let accruedBorrowBalance = getAccruedBorrowBalance(state, account); + assert(isPositive(accruedBorrowBalance), 'No active borrow'); + + let newTotalBorrows; + + if (amount.isGreaterThan(accruedBorrowBalance)) { + // repay full borrow + delete state.borrowIndexSnapshots[account]; + delete state.borrowBalances[account]; + state.totalBorrows = state.totalBorrows.minus(accruedBorrowBalance); + } else { + state.borrowIndexSnapshots[account] = state.borrowIndex; + state.borrowBalances[account] = accruedBorrowBalance.minus(amount); + state.totalBorrows = state.totalBorrows.minus(amount); + } + + return { + ...state, + totalCash: state.totalCash.plus(bn.min(amount, accruedBorrowBalance)) + }; + }, + gen: globals => { + return { + account: randAccount(globals), + amount: rand(1000) + }; + } + }; + + let mint = { + action: 'mint', + rate: 10, + run: (globals, state, { account, amount }, { assert }) => { + amount = new bn(amount); + state = accrueInterest(globals, state); + state = supplierFlywheelByIndex(globals, state, account); + + let balance = get(state.balances[account]); + let exchangeRate = getExchangeRate(state); + let tokens = amount.div(exchangeRate); + return { + ...state, + totalCash: state.totalCash.plus(amount), // ignores transfer fees + totalSupply: state.totalSupply.plus(tokens), + balances: { + ...state.balances, + [account]: balance.plus(tokens) + } + }; + }, + gen: globals => { + return { + account: randAccount(globals), + amount: rand(1000) + }; + } + }; + + let redeem = { + action: 'redeem', + rate: 10, + run: (globals, state, { account, tokens }, { assert }) => { + tokens = new bn(tokens); + state = accrueInterest(globals, state); + state = supplierFlywheelByIndex(globals, state, account); + + let balance = get(state.balances[account]); + assert(balance.isGreaterThan(tokens), 'Redeem fails for insufficient balance'); + let exchangeRate = getExchangeRate(state); + let amount = tokens.times(exchangeRate); + + return { + ...state, + totalCash: state.totalCash.minus(amount), // ignores transfer fees + totalSupply: state.totalSupply.minus(tokens), + balances: { + ...state.balances, + [account]: balance.minus(tokens) + } + }; + }, + gen: globals => { + return { + account: randAccount(globals), + tokens: rand(1000) + }; + } + }; + + let actors = { + mine, + mint, + redeem, + gift, + borrow, + repayBorrow + // test + }; + + let generateGlobals = () => { + return { + blockNumber: new bn(1000), + accounts: ['Adam Savage', 'Ben Solo', 'Jeffrey Lebowski'] + }; + }; + + // assert amount distributed by the crank is expected, that it equals # blocks with a supply * comp speed + let crankCorrectnessInvariant = (globals, state, events, invariantFn) => { + let expected = state.activeSupplyBlocks + .times(state.compSupplySpeed) + .plus(state.activeBorrowBlocks.times(state.compBorrowSpeed)); + + let actual = Object.values(state.compAccruedWithCrank).reduce( + (acc, val) => acc.plus(val), + new bn(0) + ); + invariantFn( + almostEqual, + expected, + actual, + `crank method distributed comp inaccurately` + ); + }; + + // assert comp distributed by index is the same as amount distributed by crank + let indexCorrectnessInvariant = (globals, state, events, invariantFn) => { + let expected = state.compAccruedWithCrank; + let actual = state.compAccruedWithIndex; + invariantFn( + (expected, actual) => { + return Object.keys(expected).reduce((succeeded, account) => { + return ( + almostEqual(get(expected[account]), get(actual[account])) && + succeeded + ); + }, true); + }, + expected, + actual, + `crank method does not match index method` + ); + }; + + let testInvariants = (globals, state, events, invariantFn) => { + crankCorrectnessInvariant(globals, state, events, invariantFn); + indexCorrectnessInvariant(globals, state, events, invariantFn); + }; + + let randActor = () => { + // TODO: Handle weighting actors + let actorKeys = Object.keys(actors); + let actorsLen = actorKeys.length; + return actors[actorKeys[rand(actorsLen)]]; + }; + + let executeAction = (globals, state, event, i) => { + const prevState = deepCopy(state); + assert = (assertion, reason) => { + if (!assertion) { + throw new AssertionError(assertion, reason, event, i); + } + }; + + try { + return actors[event.action].run(globals, state, event, { assert }); + } catch (e) { + if (e instanceof AssertionError) { + // TODO: ignore e! + console.debug(`assertion failed: ${e.toString()}`); + event.failed = true; + return prevState; + } else { + throw e; + } + } finally { + if (event.mine) { + globals.blockNumber = globals.blockNumber.plus(event.mine); + } + } + }; + + let runEvents = (globals, initState, events) => { + let state = events.reduce(executeAction.bind(null, globals), initState); + return trueUpComp(globals, state); + }; + + let generateEvent = globals => { + let actor = randActor(); + + return { + ...actor.gen(globals), + action: actor.action + }; + }; + + let generateEvents = (globals, count) => { + return range(count).map(() => { + return generateEvent(globals); + }); + }; + + function go(invariantFn) { + let globals = generateGlobals(); + let initState = initialState(globals); + let events = generateEvents(globals, NUM_EVENTS); + let state = runEvents(globals, initState, events); + + let invariantFnBound = (assertion, expected, actual, reason) => { + invariantFn(assertion, expected, actual, reason, state, events); + }; + + testInvariants(globals, state, events, invariantFnBound); + } + + range(RUN_COUNT).forEach(count => { + it(`runs: ${count}`, () => { + let invariant = (assertion, expected, actual, reason, state, events) => { + expect(assertion).toFuzzPass(expected, actual, reason, state, events); + }; + + go(invariant); + }); + }); +}); diff --git a/tests/Governance/GovernorAlpha/StateTest.js b/tests/Governance/GovernorAlpha/StateTest.js index e19948ea6..d35392852 100644 --- a/tests/Governance/GovernorAlpha/StateTest.js +++ b/tests/Governance/GovernorAlpha/StateTest.js @@ -1,6 +1,6 @@ const { advanceBlocks, - bigNumberify, + etherUnsigned, both, encodeParameters, etherMantissa, @@ -31,7 +31,7 @@ describe('GovernorAlpha#state/1', () => { await freezeTime(100); [root, acct, ...accounts] = accounts; comp = await deploy('Comp', [root]); - delay = bigNumberify(2 * 24 * 60 * 60).mul(2) + delay = etherUnsigned(2 * 24 * 60 * 60).mul(2) timelock = await deploy('TimelockHarness', [root, delay]); gov = await deploy('GovernorAlpha', [timelock._address, comp._address, root]); await send(timelock, "harnessSetAdmin", [gov._address]) @@ -119,7 +119,7 @@ describe('GovernorAlpha#state/1', () => { let gracePeriod = await call(timelock, 'GRACE_PERIOD') let p = await call(gov, "proposals", [newProposalId]); - let eta = bigNumberify(p.eta) + let eta = etherUnsigned(p.eta) await freezeTime(eta.add(gracePeriod).sub(1).toNumber()) @@ -142,7 +142,7 @@ describe('GovernorAlpha#state/1', () => { let gracePeriod = await call(timelock, 'GRACE_PERIOD') let p = await call(gov, "proposals", [newProposalId]); - let eta = bigNumberify(p.eta) + let eta = etherUnsigned(p.eta) await freezeTime(eta.add(gracePeriod).sub(1).toNumber()) diff --git a/tests/TimelockTest.js b/tests/TimelockTest.js index 602062b12..60ed09898 100644 --- a/tests/TimelockTest.js +++ b/tests/TimelockTest.js @@ -1,12 +1,12 @@ const { encodeParameters, - bigNumberify, + etherUnsigned, freezeTime, keccak256 } = require('./Utils/Ethereum'); -const oneWeekInSeconds = bigNumberify(7 * 24 * 60 * 60); -const zero = bigNumberify(0); +const oneWeekInSeconds = etherUnsigned(7 * 24 * 60 * 60); +const zero = etherUnsigned(0); const gracePeriod = oneWeekInSeconds.mul(2); describe('Timelock', () => { @@ -19,7 +19,7 @@ describe('Timelock', () => { let value = zero; let signature = 'setDelay(uint256)'; let data = encodeParameters(['uint256'], [newDelay]); - let revertData = encodeParameters(['uint256'], [bigNumberify(60 * 60)]); + let revertData = encodeParameters(['uint256'], [etherUnsigned(60 * 60)]); let eta; let queuedTxHash; @@ -27,7 +27,7 @@ describe('Timelock', () => { [root, notAdmin, newAdmin] = accounts; timelock = await deploy('TimelockHarness', [root, delay]); - blockTimestamp = bigNumberify(100); + blockTimestamp = etherUnsigned(100); await freezeTime(blockTimestamp.toNumber()) target = timelock.options.address; eta = blockTimestamp.add(delay); @@ -286,7 +286,7 @@ describe('Timelock', () => { beforeEach(async () => { const configuredDelay = await call(timelock, 'delay'); - delay = bigNumberify(configuredDelay); + delay = etherUnsigned(configuredDelay); signature = 'setPendingAdmin(address)'; data = encodeParameters(['address'], [newAdmin]); eta = blockTimestamp.add(delay); diff --git a/tests/Utils/Compound.js b/tests/Utils/Compound.js index 6979f8a41..3e95dc294 100644 --- a/tests/Utils/Compound.js +++ b/tests/Utils/Compound.js @@ -5,13 +5,14 @@ const { encodeParameters, etherBalance, etherMantissa, - etherUnsigned + etherUnsigned, + mergeInterface } = require('./Ethereum'); async function makeComptroller(opts = {}) { const { root = saddle.account, - kind = 'unitroller-v1' + kind = 'unitroller' } = opts || {}; if (kind == 'bool') { @@ -32,14 +33,12 @@ async function makeComptroller(opts = {}) { await send(comptroller, '_setMaxAssets', [maxAssets]); await send(comptroller, '_setPriceOracle', [priceOracle._address]); - comptroller.options.address = comptroller._address; - return Object.assign(comptroller, { priceOracle }); } - if (kind == 'unitroller-v1') { - const unitroller = await deploy('Unitroller'); - const comptroller = await deploy('ComptrollerHarness'); + if (kind == 'unitroller-prior') { + const unitroller = opts.unitroller || await deploy('Unitroller'); + const comptroller = await deploy('ComptrollerG2'); const priceOracle = opts.priceOracle || await makePriceOracle(opts.priceOracleOpts); const closeFactor = etherMantissa(dfn(opts.closeFactor, .051)); const maxAssets = etherUnsigned(dfn(opts.maxAssets, 10)); @@ -47,13 +46,36 @@ async function makeComptroller(opts = {}) { await send(unitroller, '_setPendingImplementation', [comptroller._address]); await send(comptroller, '_become', [unitroller._address]); - comptroller.options.address = unitroller._address; - await send(comptroller, '_setLiquidationIncentive', [liquidationIncentive]); - await send(comptroller, '_setCloseFactor', [closeFactor]); - await send(comptroller, '_setMaxAssets', [maxAssets]); - await send(comptroller, '_setPriceOracle', [priceOracle._address]); + mergeInterface(unitroller, comptroller); + await send(unitroller, '_setLiquidationIncentive', [liquidationIncentive]); + await send(unitroller, '_setCloseFactor', [closeFactor]); + await send(unitroller, '_setMaxAssets', [maxAssets]); + await send(unitroller, '_setPriceOracle', [priceOracle._address]); - return Object.assign(comptroller, { priceOracle }); + return Object.assign(unitroller, { priceOracle }); + } + + if (kind == 'unitroller') { + const unitroller = opts.unitroller || await deploy('Unitroller'); + const comptroller = await deploy('ComptrollerHarness'); + const priceOracle = opts.priceOracle || await makePriceOracle(opts.priceOracleOpts); + const closeFactor = etherMantissa(dfn(opts.closeFactor, .051)); + const maxAssets = etherUnsigned(dfn(opts.maxAssets, 10)); + const liquidationIncentive = etherMantissa(1); + const comp = opts.comp || await deploy('Comp', [opts.compOwner || root]); + const compRate = etherUnsigned(dfn(opts.compRate, 1e18)); + const compMarkets = opts.compMarkets || []; + + await send(unitroller, '_setPendingImplementation', [comptroller._address]); + await send(comptroller, '_become', [unitroller._address, compRate, compMarkets]); + mergeInterface(unitroller, comptroller); + await send(unitroller, '_setLiquidationIncentive', [liquidationIncentive]); + await send(unitroller, '_setCloseFactor', [closeFactor]); + await send(unitroller, '_setMaxAssets', [maxAssets]); + await send(unitroller, '_setPriceOracle', [priceOracle._address]); + await send(unitroller, 'setCompAddress', [comp._address]); // harness only + + return Object.assign(unitroller, { priceOracle, comp }); } } @@ -126,7 +148,7 @@ async function makeCToken(opts = {}) { cDelegatee._address, "0x0" ] - ); + ); cToken = await saddle.getContractAt('CErc20DelegateHarness', cDelegator._address); // XXXS at break; } @@ -135,6 +157,10 @@ async function makeCToken(opts = {}) { await send(comptroller, '_supportMarket', [cToken._address]); } + if (opts.addCompMarket) { + await send(comptroller, '_addCompMarket', [cToken._address]); + } + if (opts.underlyingPrice) { const price = etherMantissa(opts.underlyingPrice); await send(comptroller.priceOracle, 'setUnderlyingPrice', [cToken._address, price]); diff --git a/tests/Utils/Ethereum.js b/tests/Utils/Ethereum.js index 7c9ab324c..2e8849400 100644 --- a/tests/Utils/Ethereum.js +++ b/tests/Utils/Ethereum.js @@ -7,10 +7,6 @@ function address(n) { return `0x${n.toString(16).padStart(40, '0')}`; } -function bigNumberify(num) { - return ethers.utils.bigNumberify(new BigNum(num).toFixed()); -} - function encodeParameters(types, values) { const abi = new ethers.utils.AbiCoder(); return abi.encode(types, values); @@ -27,16 +23,33 @@ async function etherGasCost(receipt) { return ethers.utils.bigNumberify(gasUsed.times(gasPrice).toFixed()); } -function etherMantissa(num) { +function etherExp(num) { return etherMantissa(num, 1e18) } +function etherDouble(num) { return etherMantissa(num, 1e36) } +function etherMantissa(num, scale = 1e18) { if (num < 0) return ethers.utils.bigNumberify(new BigNum(2).pow(256).plus(num).toFixed()); - return ethers.utils.bigNumberify(new BigNum(num).times(1e18).toFixed()); + return ethers.utils.bigNumberify(new BigNum(num).times(scale).toFixed()); } function etherUnsigned(num) { return ethers.utils.bigNumberify(new BigNum(num).toFixed()); } +function mergeInterface(into, from) { + const key = (item) => item.inputs ? `${item.name}/${item.inputs.length}` : item.name; + const existing = into.options.jsonInterface.reduce((acc, item) => { + acc[key(item)] = true; + return acc; + }, {}); + const extended = from.options.jsonInterface.reduce((acc, item) => { + if (!(key(item) in existing)) + acc.push(item) + return acc; + }, into.options.jsonInterface.slice()); + into.options.jsonInterface = into.options.jsonInterface.concat(from.options.jsonInterface); + return into; +} + function getContractDefaults() { return { gas: 20000000, gasPrice: 20000 }; } @@ -113,12 +126,14 @@ async function sendFallback(contract, opts = {}) { module.exports = { address, - bigNumberify, encodeParameters, etherBalance, etherGasCost, + etherExp, + etherDouble, etherMantissa, etherUnsigned, + mergeInterface, keccak256, unlockedAccounts, unlockedAccount,