From 06ebb99b035fff08a9b25c38be083460106ec6bf Mon Sep 17 00:00:00 2001 From: bun919tw Date: Fri, 1 Oct 2021 18:03:45 +0800 Subject: [PATCH 1/3] contract/: add CToken check repay --- contracts/CCollateralCapErc20CheckRepay.sol | 812 +++++++++++ .../CCollateralCapErc20CheckRepayDelegate.sol | 52 + contracts/CTokenCheckRepay.sol | 1211 +++++++++++++++++ 3 files changed, 2075 insertions(+) create mode 100644 contracts/CCollateralCapErc20CheckRepay.sol create mode 100644 contracts/CCollateralCapErc20CheckRepayDelegate.sol create mode 100644 contracts/CTokenCheckRepay.sol diff --git a/contracts/CCollateralCapErc20CheckRepay.sol b/contracts/CCollateralCapErc20CheckRepay.sol new file mode 100644 index 000000000..452815f70 --- /dev/null +++ b/contracts/CCollateralCapErc20CheckRepay.sol @@ -0,0 +1,812 @@ +pragma solidity ^0.5.16; + +import "./CToken.sol"; +import "./CTokenCheckRepay.sol"; +import "./ERC3156FlashLenderInterface.sol"; +import "./ERC3156FlashBorrowerInterface.sol"; + +/** + * @title Cream's CCollateralCapErc20CheckRepay Contract + * @notice CTokens which wrap an EIP-20 underlying with collateral cap + * @author Cream + */ +contract CCollateralCapErc20CheckRepay is CTokenCheckRepay, CCollateralCapErc20Interface { + /** + * @notice Initialize the new money market + * @param underlying_ The address of the underlying asset + * @param comptroller_ The address of the Comptroller + * @param interestRateModel_ The address of the interest rate model + * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 + * @param name_ ERC-20 name of this token + * @param symbol_ ERC-20 symbol of this token + * @param decimals_ ERC-20 decimal precision of this token + */ + function initialize( + address underlying_, + ComptrollerInterface comptroller_, + InterestRateModel interestRateModel_, + uint256 initialExchangeRateMantissa_, + string memory name_, + string memory symbol_, + uint8 decimals_ + ) public { + // CToken initialize does the bulk of the work + super.initialize(comptroller_, interestRateModel_, initialExchangeRateMantissa_, name_, symbol_, decimals_); + + // Set underlying and sanity check it + underlying = underlying_; + EIP20Interface(underlying).totalSupply(); + } + + /*** User Interface ***/ + + /** + * @notice Sender supplies assets into the market and receives cTokens in exchange + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param mintAmount The amount of the underlying asset to supply + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function mint(uint256 mintAmount) external returns (uint256) { + (uint256 err, ) = mintInternal(mintAmount, false); + return err; + } + + /** + * @notice Sender redeems cTokens in exchange for the underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemTokens The number of cTokens to redeem into underlying + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeem(uint256 redeemTokens) external returns (uint256) { + return redeemInternal(redeemTokens, false); + } + + /** + * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemAmount The amount of underlying to redeem + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemUnderlying(uint256 redeemAmount) external returns (uint256) { + return redeemUnderlyingInternal(redeemAmount, false); + } + + /** + * @notice Sender borrows assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrow(uint256 borrowAmount) external returns (uint256) { + return borrowInternal(borrowAmount, false); + } + + /** + * @notice Sender repays their own borrow + * @param repayAmount The amount to repay + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function repayBorrow(uint256 repayAmount) external returns (uint256) { + (uint256 err, ) = repayBorrowInternal(repayAmount, false); + return err; + } + + /** + * @notice Sender repays a borrow belonging to borrower + * @param borrower the account with the debt being payed off + * @param repayAmount The amount to repay + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function repayBorrowBehalf(address borrower, uint256 repayAmount) external returns (uint256) { + (uint256 err, ) = repayBorrowBehalfInternal(borrower, repayAmount, false); + return err; + } + + /** + * @notice The sender liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param repayAmount The amount of the underlying borrowed asset to repay + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function liquidateBorrow( + address borrower, + uint256 repayAmount, + CTokenInterface cTokenCollateral + ) external returns (uint256) { + (uint256 err, ) = liquidateBorrowInternal(borrower, repayAmount, cTokenCollateral, false); + return err; + } + + /** + * @notice The sender adds to reserves. + * @param addAmount The amount fo underlying token to add as reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _addReserves(uint256 addAmount) external returns (uint256) { + return _addReservesInternal(addAmount, false); + } + + /** + * @notice Set the given collateral cap for the market. + * @param newCollateralCap New collateral cap for this market. A value of 0 corresponds to no cap. + */ + function _setCollateralCap(uint256 newCollateralCap) external { + require(msg.sender == admin, "only admin can set collateral cap"); + + collateralCap = newCollateralCap; + emit NewCollateralCap(address(this), newCollateralCap); + } + + /** + * @notice Absorb excess cash into reserves. + */ + function gulp() external nonReentrant { + uint256 cashOnChain = getCashOnChain(); + uint256 cashPrior = getCashPrior(); + + uint256 excessCash = sub_(cashOnChain, cashPrior); + totalReserves = add_(totalReserves, excessCash); + internalCash = cashOnChain; + } + + /** + * @notice Get the max flash loan amount + */ + function maxFlashLoan() external view returns (uint256) { + uint256 amount = 0; + if ( + ComptrollerInterfaceExtension(address(comptroller)).flashloanAllowed(address(this), address(0), amount, "") + ) { + amount = getCashPrior(); + } + return amount; + } + + /** + * @notice Get the flash loan fees + * @param amount amount of token to borrow + */ + function flashFee(uint256 amount) external view returns (uint256) { + require( + ComptrollerInterfaceExtension(address(comptroller)).flashloanAllowed(address(this), address(0), amount, ""), + "flashloan is paused" + ); + return div_(mul_(amount, flashFeeBips), 10000); + } + + /** + * @notice Flash loan funds to a given account. + * @param receiver The receiver address for the funds + * @param initiator flash loan initiator + * @param amount The amount of the funds to be loaned + * @param data The other data + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function flashLoan( + ERC3156FlashBorrowerInterface receiver, + address initiator, + uint256 amount, + bytes calldata data + ) external nonReentrant returns (bool) { + require(amount > 0, "flashLoan amount should be greater than zero"); + require(accrueInterest() == uint256(Error.NO_ERROR), "accrue interest failed"); + require( + ComptrollerInterfaceExtension(address(comptroller)).flashloanAllowed( + address(this), + address(receiver), + amount, + data + ), + "flashloan is paused" + ); + uint256 cashOnChainBefore = getCashOnChain(); + uint256 cashBefore = getCashPrior(); + require(cashBefore >= amount, "INSUFFICIENT_LIQUIDITY"); + + // 1. calculate fee, 1 bips = 1/10000 + uint256 totalFee = this.flashFee(amount); + + // 2. transfer fund to receiver + doTransferOut(address(uint160(address(receiver))), amount, false); + + // 3. update totalBorrows + totalBorrows = add_(totalBorrows, amount); + + // 4. execute receiver's callback function + + require( + receiver.onFlashLoan(initiator, underlying, amount, totalFee, data) == + keccak256("ERC3156FlashBorrowerInterface.onFlashLoan"), + "IERC3156: Callback failed" + ); + + // 5. take amount + fee from receiver, then check balance + uint256 repaymentAmount = add_(amount, totalFee); + doTransferIn(address(receiver), repaymentAmount, false); + + uint256 cashOnChainAfter = getCashOnChain(); + + require(cashOnChainAfter == add_(cashOnChainBefore, totalFee), "BALANCE_INCONSISTENT"); + + // 6. update reserves and internal cash and totalBorrows + uint256 reservesFee = mul_ScalarTruncate(Exp({mantissa: reserveFactorMantissa}), totalFee); + totalReserves = add_(totalReserves, reservesFee); + internalCash = add_(cashBefore, totalFee); + totalBorrows = sub_(totalBorrows, amount); + + emit Flashloan(address(receiver), amount, totalFee, reservesFee); + return true; + } + + /** + * @notice Register account collateral tokens if there is space. + * @param account The account to register + * @dev This function could only be called by comptroller. + * @return The actual registered amount of collateral + */ + function registerCollateral(address account) external returns (uint256) { + // Make sure accountCollateralTokens of `account` is initialized. + initializeAccountCollateralTokens(account); + + require(msg.sender == address(comptroller), "only comptroller may register collateral for user"); + + uint256 amount = sub_(accountTokens[account], accountCollateralTokens[account]); + return increaseUserCollateralInternal(account, amount); + } + + /** + * @notice Unregister account collateral tokens if the account still has enough collateral. + * @dev This function could only be called by comptroller. + * @param account The account to unregister + */ + function unregisterCollateral(address account) external { + // Make sure accountCollateralTokens of `account` is initialized. + initializeAccountCollateralTokens(account); + + require(msg.sender == address(comptroller), "only comptroller may unregister collateral for user"); + require( + comptroller.redeemAllowed(address(this), account, accountCollateralTokens[account]) == 0, + "comptroller rejection" + ); + + decreaseUserCollateralInternal(account, accountCollateralTokens[account]); + } + + /*** Safe Token ***/ + + /** + * @notice Gets internal balance of this contract in terms of the underlying. + * It excludes balance from direct transfer. + * @dev This excludes the value of the current message, if any + * @return The quantity of underlying tokens owned by this contract + */ + function getCashPrior() internal view returns (uint256) { + return internalCash; + } + + /** + * @notice Gets total balance of this contract in terms of the underlying + * @dev This excludes the value of the current message, if any + * @return The quantity of underlying tokens owned by this contract + */ + function getCashOnChain() internal view returns (uint256) { + EIP20Interface token = EIP20Interface(underlying); + return token.balanceOf(address(this)); + } + + /** + * @notice Initialize the account's collateral tokens. This function should be called in the beginning of every function + * that accesses accountCollateralTokens or accountTokens. + * @param account The account of accountCollateralTokens that needs to be updated + */ + function initializeAccountCollateralTokens(address account) internal { + /** + * If isCollateralTokenInit is false, it means accountCollateralTokens was not initialized yet. + * This case will only happen once and must be the very beginning. accountCollateralTokens is a new structure and its + * initial value should be equal to accountTokens if user has entered the market. However, it's almost impossible to + * check every user's value when the implementation becomes active. Therefore, it must rely on every action which will + * access accountTokens to call this function to check if accountCollateralTokens needed to be initialized. + */ + if (!isCollateralTokenInit[account]) { + if (ComptrollerInterfaceExtension(address(comptroller)).checkMembership(account, CToken(address(this)))) { + accountCollateralTokens[account] = accountTokens[account]; + totalCollateralTokens = add_(totalCollateralTokens, accountTokens[account]); + + emit UserCollateralChanged(account, accountCollateralTokens[account]); + } + isCollateralTokenInit[account] = true; + } + } + + /** + * @dev Similar to EIP20 transfer, except it handles a False result from `transferFrom` and reverts in that case. + * This will revert due to insufficient balance or insufficient allowance. + * This function returns the actual amount received, + * which may be less than `amount` if there is a fee attached to the transfer. + * + * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. + * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferIn( + address from, + uint256 amount, + bool isNative + ) internal returns (uint256) { + isNative; // unused + + EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); + uint256 balanceBefore = EIP20Interface(underlying).balanceOf(address(this)); + token.transferFrom(from, address(this), amount); + + bool success; + assembly { + switch returndatasize() + case 0 { + // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { + // This is a compliant ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of external call + } + default { + // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + require(success, "TOKEN_TRANSFER_IN_FAILED"); + + // Calculate the amount that was *actually* transferred + uint256 balanceAfter = EIP20Interface(underlying).balanceOf(address(this)); + uint256 transferredIn = sub_(balanceAfter, balanceBefore); + internalCash = add_(internalCash, transferredIn); + return transferredIn; + } + + /** + * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory + * error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to + * insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified + * it is >= amount, this should not revert in normal conditions. + * + * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. + * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferOut( + address payable to, + uint256 amount, + bool isNative + ) internal { + isNative; // unused + + EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); + token.transfer(to, amount); + + bool success; + assembly { + switch returndatasize() + case 0 { + // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { + // This is a complaint ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of external call + } + default { + // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + require(success, "TOKEN_TRANSFER_OUT_FAILED"); + internalCash = sub_(internalCash, amount); + } + + /** + * @notice Transfer `tokens` tokens from `src` to `dst` by `spender` + * @dev Called by both `transfer` and `transferFrom` internally + * @param spender The address of the account performing the transfer + * @param src The address of the source account + * @param dst The address of the destination account + * @param tokens The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transferTokens( + address spender, + address src, + address dst, + uint256 tokens + ) internal returns (uint256) { + // Make sure accountCollateralTokens of `src` and `dst` are initialized. + initializeAccountCollateralTokens(src); + initializeAccountCollateralTokens(dst); + + /** + * For every user, accountTokens must be greater than or equal to accountCollateralTokens. + * The buffer between the two values will be transferred first. + * bufferTokens = accountTokens[src] - accountCollateralTokens[src] + * collateralTokens = tokens - bufferTokens + */ + uint256 bufferTokens = sub_(accountTokens[src], accountCollateralTokens[src]); + uint256 collateralTokens = 0; + if (tokens > bufferTokens) { + collateralTokens = tokens - bufferTokens; + } + + /** + * Since bufferTokens are not collateralized and can be transferred freely, we only check with comptroller + * whether collateralized tokens can be transferred. + */ + uint256 allowed = comptroller.transferAllowed(address(this), src, dst, collateralTokens); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.TRANSFER_COMPTROLLER_REJECTION, allowed); + } + + /* Do not allow self-transfers */ + if (src == dst) { + return fail(Error.BAD_INPUT, FailureInfo.TRANSFER_NOT_ALLOWED); + } + + /* Get the allowance, infinite for the account owner */ + uint256 startingAllowance = 0; + if (spender == src) { + startingAllowance = uint256(-1); + } else { + startingAllowance = transferAllowances[src][spender]; + } + + /* Do the calculations, checking for {under,over}flow */ + accountTokens[src] = sub_(accountTokens[src], tokens); + accountTokens[dst] = add_(accountTokens[dst], tokens); + if (collateralTokens > 0) { + accountCollateralTokens[src] = sub_(accountCollateralTokens[src], collateralTokens); + accountCollateralTokens[dst] = add_(accountCollateralTokens[dst], collateralTokens); + + emit UserCollateralChanged(src, accountCollateralTokens[src]); + emit UserCollateralChanged(dst, accountCollateralTokens[dst]); + } + + /* Eat some of the allowance (if necessary) */ + if (startingAllowance != uint256(-1)) { + transferAllowances[src][spender] = sub_(startingAllowance, tokens); + } + + /* We emit a Transfer event */ + emit Transfer(src, dst, tokens); + + comptroller.transferVerify(address(this), src, dst, tokens); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Get the account's cToken balances + * @param account The address of the account + */ + function getCTokenBalanceInternal(address account) internal view returns (uint256) { + if (isCollateralTokenInit[account]) { + return accountCollateralTokens[account]; + } else { + /** + * If the value of accountCollateralTokens was not initialized, we should return the value of accountTokens. + */ + return accountTokens[account]; + } + } + + /** + * @notice Increase user's collateral. Increase as much as we can. + * @param account The address of the account + * @param amount The amount of collateral user wants to increase + * @return The actual increased amount of collateral + */ + function increaseUserCollateralInternal(address account, uint256 amount) internal returns (uint256) { + uint256 totalCollateralTokensNew = add_(totalCollateralTokens, amount); + if (collateralCap == 0 || (collateralCap != 0 && totalCollateralTokensNew <= collateralCap)) { + // 1. If collateral cap is not set, + // 2. If collateral cap is set but has enough space for this user, + // give all the user needs. + totalCollateralTokens = totalCollateralTokensNew; + accountCollateralTokens[account] = add_(accountCollateralTokens[account], amount); + + emit UserCollateralChanged(account, accountCollateralTokens[account]); + return amount; + } else if (collateralCap > totalCollateralTokens) { + // If the collateral cap is set but the remaining cap is not enough for this user, + // give the remaining parts to the user. + uint256 gap = sub_(collateralCap, totalCollateralTokens); + totalCollateralTokens = add_(totalCollateralTokens, gap); + accountCollateralTokens[account] = add_(accountCollateralTokens[account], gap); + + emit UserCollateralChanged(account, accountCollateralTokens[account]); + return gap; + } + return 0; + } + + /** + * @notice Decrease user's collateral. Reject if the amount can't be fully decrease. + * @param account The address of the account + * @param amount The amount of collateral user wants to decrease + */ + function decreaseUserCollateralInternal(address account, uint256 amount) internal { + /* + * Return if amount is zero. + * Put behind `redeemAllowed` for accuring potential COMP rewards. + */ + if (amount == 0) { + return; + } + + totalCollateralTokens = sub_(totalCollateralTokens, amount); + accountCollateralTokens[account] = sub_(accountCollateralTokens[account], amount); + + emit UserCollateralChanged(account, accountCollateralTokens[account]); + } + + struct MintLocalVars { + uint256 exchangeRateMantissa; + uint256 mintTokens; + uint256 actualMintAmount; + } + + /** + * @notice User supplies assets into the market and receives cTokens in exchange + * @dev Assumes interest has already been accrued up to the current block + * @param minter The address of the account which is supplying the assets + * @param mintAmount The amount of the underlying asset to supply + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. + */ + function mintFresh( + address minter, + uint256 mintAmount, + bool isNative + ) internal returns (uint256, uint256) { + // Make sure accountCollateralTokens of `minter` is initialized. + initializeAccountCollateralTokens(minter); + + /* Fail if mint not allowed */ + uint256 allowed = comptroller.mintAllowed(address(this), minter, mintAmount); + if (allowed != 0) { + return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0); + } + + /* + * Return if mintAmount is zero. + * Put behind `mintAllowed` for accuring potential COMP rewards. + */ + if (mintAmount == 0) { + return (uint256(Error.NO_ERROR), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK), 0); + } + + MintLocalVars memory vars; + + vars.exchangeRateMantissa = exchangeRateStoredInternal(); + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call `doTransferIn` for the minter and the mintAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * `doTransferIn` reverts if anything goes wrong, since we can't be sure if + * side-effects occurred. The function returns the amount actually transferred, + * in case of a fee. On success, the cToken holds an additional `actualMintAmount` + * of cash. + */ + vars.actualMintAmount = doTransferIn(minter, mintAmount, isNative); + + /* + * We get the current exchange rate and calculate the number of cTokens to be minted: + * mintTokens = actualMintAmount / exchangeRate + */ + vars.mintTokens = div_ScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa})); + + /* + * We calculate the new total supply of cTokens and minter token balance, checking for overflow: + * totalSupply = totalSupply + mintTokens + * accountTokens[minter] = accountTokens[minter] + mintTokens + */ + totalSupply = add_(totalSupply, vars.mintTokens); + accountTokens[minter] = add_(accountTokens[minter], vars.mintTokens); + + /* + * We only allocate collateral tokens if the minter has entered the market. + */ + if (ComptrollerInterfaceExtension(address(comptroller)).checkMembership(minter, CToken(address(this)))) { + increaseUserCollateralInternal(minter, vars.mintTokens); + } + + /* We emit a Mint event, and a Transfer event */ + emit Mint(minter, vars.actualMintAmount, vars.mintTokens); + emit Transfer(address(this), minter, vars.mintTokens); + + /* We call the defense hook */ + comptroller.mintVerify(address(this), minter, vars.actualMintAmount, vars.mintTokens); + + return (uint256(Error.NO_ERROR), vars.actualMintAmount); + } + + struct RedeemLocalVars { + uint256 exchangeRateMantissa; + uint256 redeemTokens; + uint256 redeemAmount; + } + + /** + * @notice User redeems cTokens in exchange for the underlying asset + * @dev Assumes interest has already been accrued up to the current block. Only one of redeemTokensIn or redeemAmountIn may be non-zero and it would do nothing if both are zero. + * @param redeemer The address of the account which is redeeming the tokens + * @param redeemTokensIn The number of cTokens to redeem into underlying + * @param redeemAmountIn The number of underlying tokens to receive from redeeming cTokens + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemFresh( + address payable redeemer, + uint256 redeemTokensIn, + uint256 redeemAmountIn, + bool isNative + ) internal returns (uint256) { + // Make sure accountCollateralTokens of `redeemer` is initialized. + initializeAccountCollateralTokens(redeemer); + + require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero"); + + RedeemLocalVars memory vars; + + /* exchangeRate = invoke Exchange Rate Stored() */ + vars.exchangeRateMantissa = exchangeRateStoredInternal(); + + /* If redeemTokensIn > 0: */ + if (redeemTokensIn > 0) { + /* + * We calculate the exchange rate and the amount of underlying to be redeemed: + * redeemTokens = redeemTokensIn + * redeemAmount = redeemTokensIn x exchangeRateCurrent + */ + vars.redeemTokens = redeemTokensIn; + vars.redeemAmount = mul_ScalarTruncate(Exp({mantissa: vars.exchangeRateMantissa}), redeemTokensIn); + } else { + /* + * We get the current exchange rate and calculate the amount to be redeemed: + * redeemTokens = redeemAmountIn / exchangeRate + * redeemAmount = redeemAmountIn + */ + vars.redeemTokens = div_ScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa})); + vars.redeemAmount = redeemAmountIn; + } + + /** + * For every user, accountTokens must be greater than or equal to accountCollateralTokens. + * The buffer between the two values will be redeemed first. + * bufferTokens = accountTokens[redeemer] - accountCollateralTokens[redeemer] + * collateralTokens = redeemTokens - bufferTokens + */ + uint256 bufferTokens = sub_(accountTokens[redeemer], accountCollateralTokens[redeemer]); + uint256 collateralTokens = 0; + if (vars.redeemTokens > bufferTokens) { + collateralTokens = vars.redeemTokens - bufferTokens; + } + + if (collateralTokens > 0) { + require(comptroller.redeemAllowed(address(this), redeemer, collateralTokens) == 0, "comptroller rejection"); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDEEM_FRESHNESS_CHECK); + } + + /* Fail gracefully if protocol has insufficient cash */ + if (getCashPrior() < vars.redeemAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDEEM_TRANSFER_OUT_NOT_POSSIBLE); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We calculate the new total supply and redeemer balance, checking for underflow: + * totalSupplyNew = totalSupply - redeemTokens + * accountTokensNew = accountTokens[redeemer] - redeemTokens + */ + totalSupply = sub_(totalSupply, vars.redeemTokens); + accountTokens[redeemer] = sub_(accountTokens[redeemer], vars.redeemTokens); + + /* + * We only deallocate collateral tokens if the redeemer needs to redeem them. + */ + decreaseUserCollateralInternal(redeemer, collateralTokens); + + /* + * We invoke doTransferOut for the redeemer and the redeemAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken has redeemAmount less of cash. + * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + */ + doTransferOut(redeemer, vars.redeemAmount, isNative); + + /* We emit a Transfer event, and a Redeem event */ + emit Transfer(redeemer, address(this), vars.redeemTokens); + emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens); + + /* We call the defense hook */ + comptroller.redeemVerify(address(this), redeemer, vars.redeemAmount, vars.redeemTokens); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Transfers collateral tokens (this market) to the liquidator. + * @dev Called only during an in-kind liquidation, or by liquidateBorrow during the liquidation of another CToken. + * Its absolutely critical to use msg.sender as the seizer cToken and not a parameter. + * @param seizerToken The contract seizing the collateral (i.e. borrowed cToken) + * @param liquidator The account receiving seized collateral + * @param borrower The account having collateral seized + * @param seizeTokens The number of cTokens to seize + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function seizeInternal( + address seizerToken, + address liquidator, + address borrower, + uint256 seizeTokens + ) internal returns (uint256) { + // Make sure accountCollateralTokens of `liquidator` and `borrower` are initialized. + initializeAccountCollateralTokens(liquidator); + initializeAccountCollateralTokens(borrower); + + /* Fail if seize not allowed */ + uint256 allowed = comptroller.seizeAllowed(address(this), seizerToken, liquidator, borrower, seizeTokens); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_SEIZE_COMPTROLLER_REJECTION, allowed); + } + + /* + * Return if seizeTokens is zero. + * Put behind `seizeAllowed` for accuring potential COMP rewards. + */ + if (seizeTokens == 0) { + return uint256(Error.NO_ERROR); + } + + /* Fail if borrower = liquidator */ + if (borrower == liquidator) { + return fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_SEIZE_LIQUIDATOR_IS_BORROWER); + } + + /* + * We calculate the new borrower and liquidator token balances and token collateral balances, failing on underflow/overflow: + * accountTokens[borrower] = accountTokens[borrower] - seizeTokens + * accountTokens[liquidator] = accountTokens[liquidator] + seizeTokens + * accountCollateralTokens[borrower] = accountCollateralTokens[borrower] - seizeTokens + * accountCollateralTokens[liquidator] = accountCollateralTokens[liquidator] + seizeTokens + */ + accountTokens[borrower] = sub_(accountTokens[borrower], seizeTokens); + accountTokens[liquidator] = add_(accountTokens[liquidator], seizeTokens); + accountCollateralTokens[borrower] = sub_(accountCollateralTokens[borrower], seizeTokens); + accountCollateralTokens[liquidator] = add_(accountCollateralTokens[liquidator], seizeTokens); + + /* Emit a Transfer, UserCollateralChanged events */ + emit Transfer(borrower, liquidator, seizeTokens); + emit UserCollateralChanged(borrower, accountCollateralTokens[borrower]); + emit UserCollateralChanged(liquidator, accountCollateralTokens[liquidator]); + + /* We call the defense hook */ + comptroller.seizeVerify(address(this), seizerToken, liquidator, borrower, seizeTokens); + + return uint256(Error.NO_ERROR); + } +} diff --git a/contracts/CCollateralCapErc20CheckRepayDelegate.sol b/contracts/CCollateralCapErc20CheckRepayDelegate.sol new file mode 100644 index 000000000..1f431438a --- /dev/null +++ b/contracts/CCollateralCapErc20CheckRepayDelegate.sol @@ -0,0 +1,52 @@ +pragma solidity ^0.5.16; + +import "./CCollateralCapErc20CheckRepay.sol"; + +/** + * @title Cream's CCollateralCapErc20CheckRepayDelegate Contract + * @notice CTokens which wrap an EIP-20 underlying and are delegated to + * @author Cream + */ +contract CCollateralCapErc20CheckRepayDelegate is CCollateralCapErc20CheckRepay { + /** + * @notice Construct an empty delegate + */ + constructor() public {} + + /** + * @notice Called by the delegator on a delegate to initialize it for duty + * @param data The encoded bytes data for any initialization + */ + function _becomeImplementation(bytes memory data) public { + // Shh -- currently unused + data; + + // Shh -- we don't ever want this hook to be marked pure + if (false) { + implementation = address(0); + } + + require(msg.sender == admin, "only the admin may call _becomeImplementation"); + + // Set internal cash when becoming implementation + internalCash = getCashOnChain(); + + // Set CToken version in comptroller + ComptrollerInterfaceExtension(address(comptroller)).updateCTokenVersion( + address(this), + ComptrollerV2Storage.Version.COLLATERALCAP + ); + } + + /** + * @notice Called by the delegator on a delegate to forfeit its responsibility + */ + function _resignImplementation() public { + // Shh -- we don't ever want this hook to be marked pure + if (false) { + implementation = address(0); + } + + require(msg.sender == admin, "only the admin may call _resignImplementation"); + } +} diff --git a/contracts/CTokenCheckRepay.sol b/contracts/CTokenCheckRepay.sol new file mode 100644 index 000000000..31d2c2390 --- /dev/null +++ b/contracts/CTokenCheckRepay.sol @@ -0,0 +1,1211 @@ +pragma solidity ^0.5.16; + +import "./ComptrollerInterface.sol"; +import "./CTokenInterfaces.sol"; +import "./ErrorReporter.sol"; +import "./Exponential.sol"; +import "./EIP20Interface.sol"; +import "./EIP20NonStandardInterface.sol"; +import "./InterestRateModel.sol"; + +/** + * @title Compound's CToken Contract + * @notice Abstract base for CTokens + * @author Compound + */ +contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { + /** + * @notice Initialize the money market + * @param comptroller_ The address of the Comptroller + * @param interestRateModel_ The address of the interest rate model + * @param initialExchangeRateMantissa_ The initial exchange rate, scaled by 1e18 + * @param name_ EIP-20 name of this token + * @param symbol_ EIP-20 symbol of this token + * @param decimals_ EIP-20 decimal precision of this token + */ + function initialize( + ComptrollerInterface comptroller_, + InterestRateModel interestRateModel_, + uint256 initialExchangeRateMantissa_, + string memory name_, + string memory symbol_, + uint8 decimals_ + ) public { + require(msg.sender == admin, "only admin may initialize the market"); + require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once"); + + // Set initial exchange rate + initialExchangeRateMantissa = initialExchangeRateMantissa_; + require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero."); + + // Set the comptroller + uint256 err = _setComptroller(comptroller_); + require(err == uint256(Error.NO_ERROR), "setting comptroller failed"); + + // Initialize block number and borrow index (block number mocks depend on comptroller being set) + accrualBlockNumber = getBlockNumber(); + borrowIndex = mantissaOne; + + // Set the interest rate model (depends on block number / borrow index) + err = _setInterestRateModelFresh(interestRateModel_); + require(err == uint256(Error.NO_ERROR), "setting interest rate model failed"); + + name = name_; + symbol = symbol_; + decimals = decimals_; + + // The counter starts true to prevent changing it from zero to non-zero (i.e. smaller cost/refund) + _notEntered = true; + } + + /** + * @notice Transfer `amount` tokens from `msg.sender` to `dst` + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transfer(address dst, uint256 amount) external nonReentrant returns (bool) { + return transferTokens(msg.sender, msg.sender, dst, amount) == uint256(Error.NO_ERROR); + } + + /** + * @notice Transfer `amount` tokens from `src` to `dst` + * @param src The address of the source account + * @param dst The address of the destination account + * @param amount The number of tokens to transfer + * @return Whether or not the transfer succeeded + */ + function transferFrom( + address src, + address dst, + uint256 amount + ) external nonReentrant returns (bool) { + return transferTokens(msg.sender, src, dst, amount) == uint256(Error.NO_ERROR); + } + + /** + * @notice Approve `spender` to transfer up to `amount` from `src` + * @dev This will overwrite the approval amount for `spender` + * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) + * @param spender The address of the account which may transfer tokens + * @param amount The number of tokens that are approved (-1 means infinite) + * @return Whether or not the approval succeeded + */ + function approve(address spender, uint256 amount) external returns (bool) { + address src = msg.sender; + transferAllowances[src][spender] = amount; + emit Approval(src, spender, amount); + return true; + } + + /** + * @notice Get the current allowance from `owner` for `spender` + * @param owner The address of the account which owns the tokens to be spent + * @param spender The address of the account which may transfer tokens + * @return The number of tokens allowed to be spent (-1 means infinite) + */ + function allowance(address owner, address spender) external view returns (uint256) { + return transferAllowances[owner][spender]; + } + + /** + * @notice Get the token balance of the `owner` + * @param owner The address of the account to query + * @return The number of tokens owned by `owner` + */ + function balanceOf(address owner) external view returns (uint256) { + return accountTokens[owner]; + } + + /** + * @notice Get the underlying balance of the `owner` + * @dev This also accrues interest in a transaction + * @param owner The address of the account to query + * @return The amount of underlying owned by `owner` + */ + function balanceOfUnderlying(address owner) external returns (uint256) { + Exp memory exchangeRate = Exp({mantissa: exchangeRateCurrent()}); + return mul_ScalarTruncate(exchangeRate, accountTokens[owner]); + } + + /** + * @notice Get a snapshot of the account's balances, and the cached exchange rate + * @dev This is used by comptroller to more efficiently perform liquidity checks. + * @param account Address of the account to snapshot + * @return (possible error, token balance, borrow balance, exchange rate mantissa) + */ + function getAccountSnapshot(address account) + external + view + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + uint256 cTokenBalance = getCTokenBalanceInternal(account); + uint256 borrowBalance = borrowBalanceStoredInternal(account); + uint256 exchangeRateMantissa = exchangeRateStoredInternal(); + + return (uint256(Error.NO_ERROR), cTokenBalance, borrowBalance, exchangeRateMantissa); + } + + /** + * @dev Function to simply retrieve block number + * This exists mainly for inheriting test contracts to stub this result. + */ + function getBlockNumber() internal view returns (uint256) { + return block.number; + } + + /** + * @notice Returns the current per-block borrow interest rate for this cToken + * @return The borrow interest rate per block, scaled by 1e18 + */ + function borrowRatePerBlock() external view returns (uint256) { + return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves); + } + + /** + * @notice Returns the current per-block supply interest rate for this cToken + * @return The supply interest rate per block, scaled by 1e18 + */ + function supplyRatePerBlock() external view returns (uint256) { + return interestRateModel.getSupplyRate(getCashPrior(), totalBorrows, totalReserves, reserveFactorMantissa); + } + + /** + * @notice Returns the estimated per-block borrow interest rate for this cToken after some change + * @return The borrow interest rate per block, scaled by 1e18 + */ + function estimateBorrowRatePerBlockAfterChange(uint256 change, bool repay) external view returns (uint256) { + uint256 cashPriorNew; + uint256 totalBorrowsNew; + + if (repay) { + cashPriorNew = add_(getCashPrior(), change); + totalBorrowsNew = sub_(totalBorrows, change); + } else { + cashPriorNew = sub_(getCashPrior(), change); + totalBorrowsNew = add_(totalBorrows, change); + } + return interestRateModel.getBorrowRate(cashPriorNew, totalBorrowsNew, totalReserves); + } + + /** + * @notice Returns the estimated per-block supply interest rate for this cToken after some change + * @return The supply interest rate per block, scaled by 1e18 + */ + function estimateSupplyRatePerBlockAfterChange(uint256 change, bool repay) external view returns (uint256) { + uint256 cashPriorNew; + uint256 totalBorrowsNew; + + if (repay) { + cashPriorNew = add_(getCashPrior(), change); + totalBorrowsNew = sub_(totalBorrows, change); + } else { + cashPriorNew = sub_(getCashPrior(), change); + totalBorrowsNew = add_(totalBorrows, change); + } + + return interestRateModel.getSupplyRate(cashPriorNew, totalBorrowsNew, totalReserves, reserveFactorMantissa); + } + + /** + * @notice Returns the current total borrows plus accrued interest + * @return The total borrows with interest + */ + function totalBorrowsCurrent() external nonReentrant returns (uint256) { + require(accrueInterest() == uint256(Error.NO_ERROR), "accrue interest failed"); + return totalBorrows; + } + + /** + * @notice Accrue interest to updated borrowIndex and then calculate account's borrow balance using the updated borrowIndex + * @param account The address whose balance should be calculated after updating borrowIndex + * @return The calculated balance + */ + function borrowBalanceCurrent(address account) external nonReentrant returns (uint256) { + require(accrueInterest() == uint256(Error.NO_ERROR), "accrue interest failed"); + return borrowBalanceStored(account); + } + + /** + * @notice Return the borrow balance of account based on stored data + * @param account The address whose balance should be calculated + * @return The calculated balance + */ + function borrowBalanceStored(address account) public view returns (uint256) { + return borrowBalanceStoredInternal(account); + } + + /** + * @notice Return the borrow balance of account based on stored data + * @param account The address whose balance should be calculated + * @return the calculated balance or 0 if error code is non-zero + */ + function borrowBalanceStoredInternal(address account) internal view returns (uint256) { + /* Get borrowBalance and borrowIndex */ + BorrowSnapshot storage borrowSnapshot = accountBorrows[account]; + + /* If borrowBalance = 0 then borrowIndex is likely also 0. + * Rather than failing the calculation with a division by 0, we immediately return 0 in this case. + */ + if (borrowSnapshot.principal == 0) { + return 0; + } + + /* Calculate new borrow balance using the interest index: + * recentBorrowBalance = borrower.borrowBalance * market.borrowIndex / borrower.borrowIndex + */ + uint256 principalTimesIndex = mul_(borrowSnapshot.principal, borrowIndex); + uint256 result = div_(principalTimesIndex, borrowSnapshot.interestIndex); + return result; + } + + /** + * @notice Accrue interest then return the up-to-date exchange rate + * @return Calculated exchange rate scaled by 1e18 + */ + function exchangeRateCurrent() public nonReentrant returns (uint256) { + require(accrueInterest() == uint256(Error.NO_ERROR), "accrue interest failed"); + return exchangeRateStored(); + } + + /** + * @notice Calculates the exchange rate from the underlying to the CToken + * @dev This function does not accrue interest before calculating the exchange rate + * @return Calculated exchange rate scaled by 1e18 + */ + function exchangeRateStored() public view returns (uint256) { + return exchangeRateStoredInternal(); + } + + /** + * @notice Calculates the exchange rate from the underlying to the CToken + * @dev This function does not accrue interest before calculating the exchange rate + * @return calculated exchange rate scaled by 1e18 + */ + function exchangeRateStoredInternal() internal view returns (uint256) { + uint256 _totalSupply = totalSupply; + if (_totalSupply == 0) { + /* + * If there are no tokens minted: + * exchangeRate = initialExchangeRate + */ + return initialExchangeRateMantissa; + } else { + /* + * Otherwise: + * exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply + */ + uint256 totalCash = getCashPrior(); + uint256 cashPlusBorrowsMinusReserves = sub_(add_(totalCash, totalBorrows), totalReserves); + uint256 exchangeRate = div_(cashPlusBorrowsMinusReserves, Exp({mantissa: _totalSupply})); + return exchangeRate; + } + } + + /** + * @notice Get cash balance of this cToken in the underlying asset + * @return The quantity of underlying asset owned by this contract + */ + function getCash() external view returns (uint256) { + return getCashPrior(); + } + + /** + * @notice Applies accrued interest to total borrows and reserves + * @dev This calculates interest accrued from the last checkpointed block + * up to the current block and writes new checkpoint to storage. + */ + function accrueInterest() public returns (uint256) { + /* Remember the initial block number */ + uint256 currentBlockNumber = getBlockNumber(); + uint256 accrualBlockNumberPrior = accrualBlockNumber; + + /* Short-circuit accumulating 0 interest */ + if (accrualBlockNumberPrior == currentBlockNumber) { + return uint256(Error.NO_ERROR); + } + + /* Read the previous values out of storage */ + uint256 cashPrior = getCashPrior(); + uint256 borrowsPrior = totalBorrows; + uint256 reservesPrior = totalReserves; + uint256 borrowIndexPrior = borrowIndex; + + /* Calculate the current borrow interest rate */ + uint256 borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior); + require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high"); + + /* Calculate the number of blocks elapsed since the last accrual */ + uint256 blockDelta = sub_(currentBlockNumber, accrualBlockNumberPrior); + + /* + * Calculate the interest accumulated into borrows and reserves and the new index: + * simpleInterestFactor = borrowRate * blockDelta + * interestAccumulated = simpleInterestFactor * totalBorrows + * totalBorrowsNew = interestAccumulated + totalBorrows + * totalReservesNew = interestAccumulated * reserveFactor + totalReserves + * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex + */ + + Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta); + uint256 interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior); + uint256 totalBorrowsNew = add_(interestAccumulated, borrowsPrior); + uint256 totalReservesNew = mul_ScalarTruncateAddUInt( + Exp({mantissa: reserveFactorMantissa}), + interestAccumulated, + reservesPrior + ); + uint256 borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior); + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We write the previously calculated values into storage */ + accrualBlockNumber = currentBlockNumber; + borrowIndex = borrowIndexNew; + totalBorrows = totalBorrowsNew; + totalReserves = totalReservesNew; + + /* We emit an AccrueInterest event */ + emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Sender supplies assets into the market and receives cTokens in exchange + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param mintAmount The amount of the underlying asset to supply + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual mint amount. + */ + function mintInternal(uint256 mintAmount, bool isNative) internal nonReentrant returns (uint256, uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.MINT_ACCRUE_INTEREST_FAILED), 0); + } + // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to + return mintFresh(msg.sender, mintAmount, isNative); + } + + /** + * @notice Sender redeems cTokens in exchange for the underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemTokens The number of cTokens to redeem into underlying + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemInternal(uint256 redeemTokens, bool isNative) internal nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed + return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); + } + // redeemFresh emits redeem-specific logs on errors, so we don't need to + return redeemFresh(msg.sender, redeemTokens, 0, isNative); + } + + /** + * @notice Sender redeems cTokens in exchange for a specified amount of underlying asset + * @dev Accrues interest whether or not the operation succeeds, unless reverted + * @param redeemAmount The amount of underlying to receive from redeeming cTokens + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function redeemUnderlyingInternal(uint256 redeemAmount, bool isNative) internal nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted redeem failed + return fail(Error(error), FailureInfo.REDEEM_ACCRUE_INTEREST_FAILED); + } + // redeemFresh emits redeem-specific logs on errors, so we don't need to + return redeemFresh(msg.sender, 0, redeemAmount, isNative); + } + + /** + * @notice Sender borrows assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrowInternal(uint256 borrowAmount, bool isNative) internal nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return fail(Error(error), FailureInfo.BORROW_ACCRUE_INTEREST_FAILED); + } + // borrowFresh emits borrow-specific logs on errors, so we don't need to + return borrowFresh(msg.sender, borrowAmount, isNative); + } + + struct BorrowLocalVars { + MathError mathErr; + uint256 accountBorrows; + uint256 accountBorrowsNew; + uint256 totalBorrowsNew; + } + + /** + * @notice Users borrow assets from the protocol to their own address + * @param borrowAmount The amount of the underlying asset to borrow + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function borrowFresh( + address payable borrower, + uint256 borrowAmount, + bool isNative + ) internal returns (uint256) { + /* Fail if borrow not allowed */ + uint256 allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount); + if (allowed != 0) { + return failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.BORROW_COMPTROLLER_REJECTION, allowed); + } + + /* + * Return if borrowAmount is zero. + * Put behind `borrowAllowed` for accuring potential COMP rewards. + */ + if (borrowAmount == 0) { + accountBorrows[borrower].interestIndex = borrowIndex; + return uint256(Error.NO_ERROR); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.BORROW_FRESHNESS_CHECK); + } + + /* Fail gracefully if protocol has insufficient underlying cash */ + if (getCashPrior() < borrowAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.BORROW_CASH_NOT_AVAILABLE); + } + + BorrowLocalVars memory vars; + + /* + * We calculate the new borrower and total borrow balances, failing on overflow: + * accountBorrowsNew = accountBorrows + borrowAmount + * totalBorrowsNew = totalBorrows + borrowAmount + */ + vars.accountBorrows = borrowBalanceStoredInternal(borrower); + vars.accountBorrowsNew = add_(vars.accountBorrows, borrowAmount); + vars.totalBorrowsNew = add_(totalBorrows, borrowAmount); + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We write the previously calculated values into storage */ + accountBorrows[borrower].principal = vars.accountBorrowsNew; + accountBorrows[borrower].interestIndex = borrowIndex; + totalBorrows = vars.totalBorrowsNew; + + /* + * We invoke doTransferOut for the borrower and the borrowAmount. + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken borrowAmount less of cash. + * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + */ + doTransferOut(borrower, borrowAmount, isNative); + + /* We emit a Borrow event */ + emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); + + /* We call the defense hook */ + comptroller.borrowVerify(address(this), borrower, borrowAmount); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Sender repays their own borrow + * @param repayAmount The amount to repay + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowInternal(uint256 repayAmount, bool isNative) internal nonReentrant returns (uint256, uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0); + } + // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to + return repayBorrowFresh(msg.sender, msg.sender, repayAmount, isNative); + } + + /** + * @notice Sender repays a borrow belonging to borrower + * @param borrower the account with the debt being payed off + * @param repayAmount The amount to repay + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowBehalfInternal( + address borrower, + uint256 repayAmount, + bool isNative + ) internal nonReentrant returns (uint256, uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed + return (fail(Error(error), FailureInfo.REPAY_BEHALF_ACCRUE_INTEREST_FAILED), 0); + } + // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to + return repayBorrowFresh(msg.sender, borrower, repayAmount, isNative); + } + + struct RepayBorrowLocalVars { + Error err; + MathError mathErr; + uint256 repayAmount; + uint256 borrowerIndex; + uint256 accountBorrows; + uint256 accountBorrowsNew; + uint256 totalBorrowsNew; + uint256 actualRepayAmount; + } + + /** + * @notice Borrows are repaid by another user (possibly the borrower). + * @param payer the account paying off the borrow + * @param borrower the account with the debt being payed off + * @param repayAmount the amount of undelrying tokens being returned + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function repayBorrowFresh( + address payer, + address borrower, + uint256 repayAmount, + bool isNative + ) internal returns (uint256, uint256) { + /* Fail if repayBorrow not allowed */ + uint256 allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount); + if (allowed != 0) { + return ( + failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), + 0 + ); + } + + /* + * Return if repayAmount is zero. + * Put behind `repayBorrowAllowed` for accuring potential COMP rewards. + */ + if (repayAmount == 0) { + accountBorrows[borrower].interestIndex = borrowIndex; + return (uint256(Error.NO_ERROR), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.REPAY_BORROW_FRESHNESS_CHECK), 0); + } + + RepayBorrowLocalVars memory vars; + + /* We remember the original borrowerIndex for verification purposes */ + vars.borrowerIndex = accountBorrows[borrower].interestIndex; + + /* We fetch the amount the borrower owes, with accumulated interest */ + vars.accountBorrows = borrowBalanceStoredInternal(borrower); + + /* If repayAmount == -1, repayAmount = accountBorrows */ + if (repayAmount == uint256(-1)) { + vars.repayAmount = vars.accountBorrows; + } else { + vars.repayAmount = repayAmount; + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call doTransferIn for the payer and the repayAmount + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken holds an additional repayAmount of cash. + * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. + * it returns the amount actually transferred, in case of a fee. + */ + vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount, isNative); + + /* + * We calculate the new borrower and total borrow balances, failing on underflow: + * accountBorrowsNew = accountBorrows - actualRepayAmount + * totalBorrowsNew = totalBorrows - actualRepayAmount + */ + vars.accountBorrowsNew = sub_(vars.accountBorrows, vars.actualRepayAmount); + vars.totalBorrowsNew = sub_(totalBorrows, vars.actualRepayAmount); + + /* We write the previously calculated values into storage */ + accountBorrows[borrower].principal = vars.accountBorrowsNew; + accountBorrows[borrower].interestIndex = borrowIndex; + totalBorrows = vars.totalBorrowsNew; + + /* We emit a RepayBorrow event */ + emit RepayBorrow(payer, borrower, vars.actualRepayAmount, vars.accountBorrowsNew, vars.totalBorrowsNew); + + /* We call the defense hook */ + comptroller.repayBorrowVerify(address(this), payer, borrower, vars.actualRepayAmount, vars.borrowerIndex); + + return (uint256(Error.NO_ERROR), vars.actualRepayAmount); + } + + /** + * @notice The sender liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param repayAmount The amount of the underlying borrowed asset to repay + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function liquidateBorrowInternal( + address borrower, + uint256 repayAmount, + CTokenInterface cTokenCollateral, + bool isNative + ) internal nonReentrant returns (uint256, uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed + return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_BORROW_INTEREST_FAILED), 0); + } + + error = cTokenCollateral.accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but we still want to log the fact that an attempted liquidation failed + return (fail(Error(error), FailureInfo.LIQUIDATE_ACCRUE_COLLATERAL_INTEREST_FAILED), 0); + } + + // liquidateBorrowFresh emits borrow-specific logs on errors, so we don't need to + return liquidateBorrowFresh(msg.sender, borrower, repayAmount, cTokenCollateral, isNative); + } + + struct LiquidateBorrowLocalVars { + uint256 repayBorrowError; + uint256 actualRepayAmount; + uint256 amountSeizeError; + uint256 seizeTokens; + } + + /** + * @notice The liquidator liquidates the borrowers collateral. + * The collateral seized is transferred to the liquidator. + * @param borrower The borrower of this cToken to be liquidated + * @param liquidator The address repaying the borrow and seizing collateral + * @param cTokenCollateral The market in which to seize collateral from the borrower + * @param repayAmount The amount of the underlying borrowed asset to repay + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. + */ + function liquidateBorrowFresh( + address liquidator, + address borrower, + uint256 repayAmount, + CTokenInterface cTokenCollateral, + bool isNative + ) internal returns (uint256, uint256) { + /* Fail if liquidate not allowed */ + uint256 allowed = comptroller.liquidateBorrowAllowed( + address(this), + address(cTokenCollateral), + liquidator, + borrower, + repayAmount + ); + if (allowed != 0) { + return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.LIQUIDATE_COMPTROLLER_REJECTION, allowed), 0); + } + + /* Verify market's block number equals current block number */ + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_FRESHNESS_CHECK), 0); + } + + /* Verify cTokenCollateral market's block number equals current block number */ + if (cTokenCollateral.accrualBlockNumber() != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.LIQUIDATE_COLLATERAL_FRESHNESS_CHECK), 0); + } + + /* Fail if borrower = liquidator */ + if (borrower == liquidator) { + return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0); + } + + /* Fail if repayAmount = 0 */ + if (repayAmount == 0) { + return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_ZERO), 0); + } + + /* Fail if repayAmount = -1 */ + if (repayAmount == uint256(-1)) { + return (fail(Error.INVALID_CLOSE_AMOUNT_REQUESTED, FailureInfo.LIQUIDATE_CLOSE_AMOUNT_IS_UINT_MAX), 0); + } + + LiquidateBorrowLocalVars memory vars; + + /* Fail if repayBorrow fails */ + (vars.repayBorrowError, vars.actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount, isNative); + if (vars.repayBorrowError != uint256(Error.NO_ERROR)) { + return (fail(Error(vars.repayBorrowError), FailureInfo.LIQUIDATE_REPAY_BORROW_FRESH_FAILED), 0); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* We calculate the number of collateral tokens that will be seized */ + (vars.amountSeizeError, vars.seizeTokens) = comptroller.liquidateCalculateSeizeTokens( + address(this), + address(cTokenCollateral), + vars.actualRepayAmount + ); + require( + vars.amountSeizeError == uint256(Error.NO_ERROR), + "LIQUIDATE_COMPTROLLER_CALCULATE_AMOUNT_SEIZE_FAILED" + ); + + /* Revert if borrower collateral token balance < seizeTokens */ + require(cTokenCollateral.balanceOf(borrower) >= vars.seizeTokens, "LIQUIDATE_SEIZE_TOO_MUCH"); + + // If this is also the collateral, run seizeInternal to avoid re-entrancy, otherwise make an external call + uint256 seizeError; + if (address(cTokenCollateral) == address(this)) { + seizeError = seizeInternal(address(this), liquidator, borrower, vars.seizeTokens); + } else { + seizeError = cTokenCollateral.seize(liquidator, borrower, vars.seizeTokens); + } + + /* Revert if seize tokens fails (since we cannot be sure of side effects) */ + require(seizeError == uint256(Error.NO_ERROR), "token seizure failed"); + + /* We emit a LiquidateBorrow event */ + emit LiquidateBorrow(liquidator, borrower, vars.actualRepayAmount, address(cTokenCollateral), vars.seizeTokens); + + /* We call the defense hook */ + comptroller.liquidateBorrowVerify( + address(this), + address(cTokenCollateral), + liquidator, + borrower, + vars.actualRepayAmount, + vars.seizeTokens + ); + + return (uint256(Error.NO_ERROR), vars.actualRepayAmount); + } + + /** + * @notice Transfers collateral tokens (this market) to the liquidator. + * @dev Will fail unless called by another cToken during the process of liquidation. + * Its absolutely critical to use msg.sender as the borrowed cToken and not a parameter. + * @param liquidator The account receiving seized collateral + * @param borrower The account having collateral seized + * @param seizeTokens The number of cTokens to seize + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function seize( + address liquidator, + address borrower, + uint256 seizeTokens + ) external nonReentrant returns (uint256) { + return seizeInternal(msg.sender, liquidator, borrower, seizeTokens); + } + + /*** Admin Functions ***/ + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setPendingAdmin(address payable newPendingAdmin) external returns (uint256) { + // Check caller = admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_PENDING_ADMIN_OWNER_CHECK); + } + + // Save current value, if any, for inclusion in log + address oldPendingAdmin = pendingAdmin; + + // Store pendingAdmin with value newPendingAdmin + pendingAdmin = newPendingAdmin; + + // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) + emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _acceptAdmin() external returns (uint256) { + // Check caller is pendingAdmin and pendingAdmin ≠ address(0) + if (msg.sender != pendingAdmin || msg.sender == address(0)) { + return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); + } + + // Save current values for inclusion in log + address oldAdmin = admin; + address oldPendingAdmin = pendingAdmin; + + // Store admin with value pendingAdmin + admin = pendingAdmin; + + // Clear the pending value + pendingAdmin = address(0); + + emit NewAdmin(oldAdmin, admin); + emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Sets a new comptroller for the market + * @dev Admin function to set a new comptroller + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setComptroller(ComptrollerInterface newComptroller) public returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_COMPTROLLER_OWNER_CHECK); + } + + ComptrollerInterface oldComptroller = comptroller; + // Ensure invoke comptroller.isComptroller() returns true + require(newComptroller.isComptroller(), "marker method returned false"); + + // Set market's comptroller to newComptroller + comptroller = newComptroller; + + // Emit NewComptroller(oldComptroller, newComptroller) + emit NewComptroller(oldComptroller, newComptroller); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice accrues interest and sets a new reserve factor for the protocol using _setReserveFactorFresh + * @dev Admin function to accrue interest and set a new reserve factor + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setReserveFactor(uint256 newReserveFactorMantissa) external nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reserve factor change failed. + return fail(Error(error), FailureInfo.SET_RESERVE_FACTOR_ACCRUE_INTEREST_FAILED); + } + // _setReserveFactorFresh emits reserve-factor-specific logs on errors, so we don't need to. + return _setReserveFactorFresh(newReserveFactorMantissa); + } + + /** + * @notice Sets a new reserve factor for the protocol (*requires fresh interest accrual) + * @dev Admin function to set a new reserve factor + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setReserveFactorFresh(uint256 newReserveFactorMantissa) internal returns (uint256) { + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_RESERVE_FACTOR_ADMIN_CHECK); + } + + // Verify market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_RESERVE_FACTOR_FRESH_CHECK); + } + + // Check newReserveFactor ≤ maxReserveFactor + if (newReserveFactorMantissa > reserveFactorMaxMantissa) { + return fail(Error.BAD_INPUT, FailureInfo.SET_RESERVE_FACTOR_BOUNDS_CHECK); + } + + uint256 oldReserveFactorMantissa = reserveFactorMantissa; + reserveFactorMantissa = newReserveFactorMantissa; + + emit NewReserveFactor(oldReserveFactorMantissa, newReserveFactorMantissa); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice Accrues interest and reduces reserves by transferring from msg.sender + * @param addAmount Amount of addition to reserves + * @param isNative The amount is in native or not + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _addReservesInternal(uint256 addAmount, bool isNative) internal nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. + return fail(Error(error), FailureInfo.ADD_RESERVES_ACCRUE_INTEREST_FAILED); + } + + // _addReservesFresh emits reserve-addition-specific logs on errors, so we don't need to. + (error, ) = _addReservesFresh(addAmount, isNative); + return error; + } + + /** + * @notice Add reserves by transferring from caller + * @dev Requires fresh interest accrual + * @param addAmount Amount of addition to reserves + * @param isNative The amount is in native or not + * @return (uint, uint) An error code (0=success, otherwise a failure (see ErrorReporter.sol for details)) and the actual amount added, net token fees + */ + function _addReservesFresh(uint256 addAmount, bool isNative) internal returns (uint256, uint256) { + // totalReserves + actualAddAmount + uint256 totalReservesNew; + uint256 actualAddAmount; + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return (fail(Error.MARKET_NOT_FRESH, FailureInfo.ADD_RESERVES_FRESH_CHECK), actualAddAmount); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + /* + * We call doTransferIn for the caller and the addAmount + * Note: The cToken must handle variations between ERC-20 and ETH underlying. + * On success, the cToken holds an additional addAmount of cash. + * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. + * it returns the amount actually transferred, in case of a fee. + */ + + actualAddAmount = doTransferIn(msg.sender, addAmount, isNative); + + totalReservesNew = add_(totalReserves, actualAddAmount); + + // Store reserves[n+1] = reserves[n] + actualAddAmount + totalReserves = totalReservesNew; + + /* Emit NewReserves(admin, actualAddAmount, reserves[n+1]) */ + emit ReservesAdded(msg.sender, actualAddAmount, totalReservesNew); + + /* Return (NO_ERROR, actualAddAmount) */ + return (uint256(Error.NO_ERROR), actualAddAmount); + } + + /** + * @notice Accrues interest and reduces reserves by transferring to admin + * @param reduceAmount Amount of reduction to reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _reduceReserves(uint256 reduceAmount) external nonReentrant returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted reduce reserves failed. + return fail(Error(error), FailureInfo.REDUCE_RESERVES_ACCRUE_INTEREST_FAILED); + } + // _reduceReservesFresh emits reserve-reduction-specific logs on errors, so we don't need to. + return _reduceReservesFresh(reduceAmount); + } + + /** + * @notice Reduces reserves by transferring to admin + * @dev Requires fresh interest accrual + * @param reduceAmount Amount of reduction to reserves + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _reduceReservesFresh(uint256 reduceAmount) internal returns (uint256) { + // totalReserves - reduceAmount + uint256 totalReservesNew; + + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.REDUCE_RESERVES_ADMIN_CHECK); + } + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.REDUCE_RESERVES_FRESH_CHECK); + } + + // Fail gracefully if protocol has insufficient underlying cash + if (getCashPrior() < reduceAmount) { + return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.REDUCE_RESERVES_CASH_NOT_AVAILABLE); + } + + // Check reduceAmount ≤ reserves[n] (totalReserves) + if (reduceAmount > totalReserves) { + return fail(Error.BAD_INPUT, FailureInfo.REDUCE_RESERVES_VALIDATION); + } + + ///////////////////////// + // EFFECTS & INTERACTIONS + // (No safe failures beyond this point) + + totalReservesNew = sub_(totalReserves, reduceAmount); + + // Store reserves[n+1] = reserves[n] - reduceAmount + totalReserves = totalReservesNew; + + // doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. + // Restrict reducing reserves in native token. Implementations except `CWrappedNative` won't use parameter `isNative`. + doTransferOut(admin, reduceAmount, true); + + emit ReservesReduced(admin, reduceAmount, totalReservesNew); + + return uint256(Error.NO_ERROR); + } + + /** + * @notice accrues interest and updates the interest rate model using _setInterestRateModelFresh + * @dev Admin function to accrue interest and update the interest rate model + * @param newInterestRateModel the new interest rate model to use + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setInterestRateModel(InterestRateModel newInterestRateModel) public returns (uint256) { + uint256 error = accrueInterest(); + if (error != uint256(Error.NO_ERROR)) { + // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted change of interest rate model failed + return fail(Error(error), FailureInfo.SET_INTEREST_RATE_MODEL_ACCRUE_INTEREST_FAILED); + } + // _setInterestRateModelFresh emits interest-rate-model-update-specific logs on errors, so we don't need to. + return _setInterestRateModelFresh(newInterestRateModel); + } + + /** + * @notice updates the interest rate model (*requires fresh interest accrual) + * @dev Admin function to update the interest rate model + * @param newInterestRateModel the new interest rate model to use + * @return uint 0=success, otherwise a failure (see ErrorReporter.sol for details) + */ + function _setInterestRateModelFresh(InterestRateModel newInterestRateModel) internal returns (uint256) { + // Used to store old model for use in the event that is emitted on success + InterestRateModel oldInterestRateModel; + + // Check caller is admin + if (msg.sender != admin) { + return fail(Error.UNAUTHORIZED, FailureInfo.SET_INTEREST_RATE_MODEL_OWNER_CHECK); + } + + // We fail gracefully unless market's block number equals current block number + if (accrualBlockNumber != getBlockNumber()) { + return fail(Error.MARKET_NOT_FRESH, FailureInfo.SET_INTEREST_RATE_MODEL_FRESH_CHECK); + } + + // Track the market's current interest rate model + oldInterestRateModel = interestRateModel; + + // Ensure invoke newInterestRateModel.isInterestRateModel() returns true + require(newInterestRateModel.isInterestRateModel(), "marker method returned false"); + + // Set the interest rate model to newInterestRateModel + interestRateModel = newInterestRateModel; + + // Emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel) + emit NewMarketInterestRateModel(oldInterestRateModel, newInterestRateModel); + + return uint256(Error.NO_ERROR); + } + + /*** Safe Token ***/ + + /** + * @notice Gets balance of this contract in terms of the underlying + * @dev This excludes the value of the current message, if any + * @return The quantity of underlying owned by this contract + */ + function getCashPrior() internal view returns (uint256); + + /** + * @dev Performs a transfer in, reverting upon failure. Returns the amount actually transferred to the protocol, in case of a fee. + * This may revert due to insufficient balance or insufficient allowance. + */ + function doTransferIn( + address from, + uint256 amount, + bool isNative + ) internal returns (uint256); + + /** + * @dev Performs a transfer out, ideally returning an explanatory error code upon failure tather than reverting. + * If caller has not called checked protocol's balance, may revert due to insufficient cash held in the contract. + * If caller has checked protocol's balance, and verified it is >= amount, this should not revert in normal conditions. + */ + function doTransferOut( + address payable to, + uint256 amount, + bool isNative + ) internal; + + /** + * @notice Transfer `tokens` tokens from `src` to `dst` by `spender` + * @dev Called by both `transfer` and `transferFrom` internally + */ + function transferTokens( + address spender, + address src, + address dst, + uint256 tokens + ) internal returns (uint256); + + /** + * @notice Get the account's cToken balances + */ + function getCTokenBalanceInternal(address account) internal view returns (uint256); + + /** + * @notice User supplies assets into the market and receives cTokens in exchange + * @dev Assumes interest has already been accrued up to the current block + */ + function mintFresh( + address minter, + uint256 mintAmount, + bool isNative + ) internal returns (uint256, uint256); + + /** + * @notice User redeems cTokens in exchange for the underlying asset + * @dev Assumes interest has already been accrued up to the current block + */ + function redeemFresh( + address payable redeemer, + uint256 redeemTokensIn, + uint256 redeemAmountIn, + bool isNative + ) internal returns (uint256); + + /** + * @notice Transfers collateral tokens (this market) to the liquidator. + * @dev Called only during an in-kind liquidation, or by liquidateBorrow during the liquidation of another CToken. + * Its absolutely critical to use msg.sender as the seizer cToken and not a parameter. + */ + function seizeInternal( + address seizerToken, + address liquidator, + address borrower, + uint256 seizeTokens + ) internal returns (uint256); + + /*** Reentrancy Guard ***/ + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + */ + modifier nonReentrant() { + require(_notEntered, "re-entered"); + _notEntered = false; + _; + _notEntered = true; // get a gas-refund post-Istanbul + } +} From 572496d6d66cba2c883c04cef4576f3a589d1042 Mon Sep 17 00:00:00 2001 From: bun919tw Date: Wed, 6 Oct 2021 18:22:30 +0800 Subject: [PATCH 2/3] contracts/: add protection for reenty liquidation attack for erc777 --- contracts/CTokenCheckRepay.sol | 27 +++++++++++++++++++++++---- contracts/ComptrollerInterface.sol | 9 +++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/contracts/CTokenCheckRepay.sol b/contracts/CTokenCheckRepay.sol index 31d2c2390..8f0f0b454 100644 --- a/contracts/CTokenCheckRepay.sol +++ b/contracts/CTokenCheckRepay.sol @@ -538,7 +538,7 @@ contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0); } // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to - return repayBorrowFresh(msg.sender, msg.sender, repayAmount, isNative); + return repayBorrowFresh(msg.sender, msg.sender, repayAmount, isNative, false); } /** @@ -559,7 +559,7 @@ contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { return (fail(Error(error), FailureInfo.REPAY_BEHALF_ACCRUE_INTEREST_FAILED), 0); } // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to - return repayBorrowFresh(msg.sender, borrower, repayAmount, isNative); + return repayBorrowFresh(msg.sender, borrower, repayAmount, isNative, false); } struct RepayBorrowLocalVars { @@ -579,13 +579,15 @@ contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { * @param borrower the account with the debt being payed off * @param repayAmount the amount of undelrying tokens being returned * @param isNative The amount is in native or not + * @param isFromLiquidation The request is from liquidation or not * @return (uint, uint) An error code (0=success, otherwise a failure, see ErrorReporter.sol), and the actual repayment amount. */ function repayBorrowFresh( address payer, address borrower, uint256 repayAmount, - bool isNative + bool isNative, + bool isFromLiquidation ) internal returns (uint256, uint256) { /* Fail if repayBorrow not allowed */ uint256 allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount); @@ -638,6 +640,17 @@ contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { */ vars.actualRepayAmount = doTransferIn(payer, vars.repayAmount, isNative); + // Only check account liquidity if the request is from liquidation to save gas. + if (isFromLiquidation) { + // Right after `doTransferIn` and before updating the storage, check the borrower's account liquidity again. + // If a reentrant call to another asset is made during transferring AMP in, a second account liquidity check + // could help prevent excessive liquidation. + (uint256 err, , uint256 shortfall) = ComptrollerInterfaceExtension(address(comptroller)) + .getAccountLiquidity(borrower); + require(err == 0, "failed to get account liquidity"); + require(shortfall > 0, "borrower has no shortfall"); + } + /* * We calculate the new borrower and total borrow balances, failing on underflow: * accountBorrowsNew = accountBorrows - actualRepayAmount @@ -755,7 +768,13 @@ contract CTokenCheckRepay is CTokenInterface, Exponential, TokenErrorReporter { LiquidateBorrowLocalVars memory vars; /* Fail if repayBorrow fails */ - (vars.repayBorrowError, vars.actualRepayAmount) = repayBorrowFresh(liquidator, borrower, repayAmount, isNative); + (vars.repayBorrowError, vars.actualRepayAmount) = repayBorrowFresh( + liquidator, + borrower, + repayAmount, + isNative, + true + ); if (vars.repayBorrowError != uint256(Error.NO_ERROR)) { return (fail(Error(vars.repayBorrowError), FailureInfo.LIQUIDATE_REPAY_BORROW_FRESH_FAILED), 0); } diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index f53c9efbb..208993a62 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -135,4 +135,13 @@ interface ComptrollerInterfaceExtension { uint256 amount, bytes calldata params ) external view returns (bool); + + function getAccountLiquidity(address account) + external + view + returns ( + uint256, + uint256, + uint256 + ); } From 1b80ad05a910a17fafb1c3910b6dd0c8c3b8ad4a Mon Sep 17 00:00:00 2001 From: bun919tw Date: Wed, 6 Oct 2021 18:23:18 +0800 Subject: [PATCH 3/3] tests/: add tests --- tests/Contracts/CErc20Harness.sol | 172 ++++++++++++++++++++++++++++-- tests/Contracts/EvilToken.sol | 74 ++++++++++++- tests/Tokens/attack.js | 81 +++++++++++++- tests/Utils/Compound.js | 16 ++- 4 files changed, 326 insertions(+), 17 deletions(-) diff --git a/tests/Contracts/CErc20Harness.sol b/tests/Contracts/CErc20Harness.sol index 85f109513..0f9ca2242 100644 --- a/tests/Contracts/CErc20Harness.sol +++ b/tests/Contracts/CErc20Harness.sol @@ -7,13 +7,14 @@ import "../../contracts/CErc20Delegate.sol"; import "../../contracts/Legacy/CSLPDelegate.sol"; import "../../contracts/Legacy/CCTokenDelegate.sol"; import "../../contracts/CCollateralCapErc20Delegate.sol"; +import "../../contracts/CCollateralCapErc20CheckRepayDelegate.sol"; import "../../contracts/Legacy/CCollateralCapErc20Delegator.sol"; import "../../contracts/CWrappedNativeDelegate.sol"; import "../../contracts/CWrappedNativeDelegator.sol"; import "./ComptrollerScenario.sol"; contract CErc20Harness is CErc20Immutable { - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; @@ -376,7 +377,7 @@ contract CErc20DelegateHarness is CErc20Delegate { event Log(string x, address y); event Log(string x, uint256 y); - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; @@ -531,6 +532,165 @@ contract CErc20DelegateHarness is CErc20Delegate { } } +contract CCollaterlaCapErc20CheckRepayDelegateHarness is CCollateralCapErc20CheckRepayDelegate { + event Log(string x, address y); + event Log(string x, uint256 y); + + uint256 public blockNumber = 100000; + uint256 harnessExchangeRate; + bool harnessExchangeRateStored; + + mapping(address => bool) public failTransferToAddresses; + + function exchangeRateStoredInternal() internal view returns (uint256) { + if (harnessExchangeRateStored) { + return harnessExchangeRate; + } + return super.exchangeRateStoredInternal(); + } + + function doTransferOut( + address payable to, + uint256 amount, + bool isNative + ) internal { + require(failTransferToAddresses[to] == false, "TOKEN_TRANSFER_OUT_FAILED"); + return super.doTransferOut(to, amount, isNative); + } + + function getBlockNumber() internal view returns (uint256) { + return blockNumber; + } + + function getBorrowRateMaxMantissa() public pure returns (uint256) { + return borrowRateMaxMantissa; + } + + function harnessSetBlockNumber(uint256 newBlockNumber) public { + blockNumber = newBlockNumber; + } + + function harnessFastForward(uint256 blocks) public { + blockNumber += blocks; + } + + function harnessSetBalance(address account, uint256 amount) external { + accountTokens[account] = amount; + } + + function harnessSetAccrualBlockNumber(uint256 _accrualblockNumber) public { + accrualBlockNumber = _accrualblockNumber; + } + + function harnessSetTotalSupply(uint256 totalSupply_) public { + totalSupply = totalSupply_; + } + + function harnessSetTotalBorrows(uint256 totalBorrows_) public { + totalBorrows = totalBorrows_; + } + + function harnessIncrementTotalBorrows(uint256 addtlBorrow_) public { + totalBorrows = totalBorrows + addtlBorrow_; + } + + function harnessSetTotalReserves(uint256 totalReserves_) public { + totalReserves = totalReserves_; + } + + function harnessExchangeRateDetails( + uint256 totalSupply_, + uint256 totalBorrows_, + uint256 totalReserves_ + ) public { + totalSupply = totalSupply_; + totalBorrows = totalBorrows_; + totalReserves = totalReserves_; + } + + function harnessSetExchangeRate(uint256 exchangeRate) public { + harnessExchangeRate = exchangeRate; + harnessExchangeRateStored = true; + } + + function harnessSetFailTransferToAddress(address _to, bool _fail) public { + failTransferToAddresses[_to] = _fail; + } + + function harnessMintFresh(address account, uint256 mintAmount) public returns (uint256) { + (uint256 err, ) = super.mintFresh(account, mintAmount, false); + return err; + } + + function harnessRedeemFresh( + address payable account, + uint256 cTokenAmount, + uint256 underlyingAmount + ) public returns (uint256) { + return super.redeemFresh(account, cTokenAmount, underlyingAmount, false); + } + + function harnessAccountBorrows(address account) public view returns (uint256 principal, uint256 interestIndex) { + BorrowSnapshot memory snapshot = accountBorrows[account]; + return (snapshot.principal, snapshot.interestIndex); + } + + function harnessSetAccountBorrows( + address account, + uint256 principal, + uint256 interestIndex + ) public { + accountBorrows[account] = BorrowSnapshot({principal: principal, interestIndex: interestIndex}); + } + + function harnessSetBorrowIndex(uint256 borrowIndex_) public { + borrowIndex = borrowIndex_; + } + + function harnessBorrowFresh(address payable account, uint256 borrowAmount) public returns (uint256) { + return borrowFresh(account, borrowAmount, false); + } + + function harnessRepayBorrowFresh( + address payer, + address account, + uint256 repayAmount + ) public returns (uint256) { + (uint256 err, ) = repayBorrowFresh(payer, account, repayAmount, false, false); + return err; + } + + function harnessLiquidateBorrowFresh( + address liquidator, + address borrower, + uint256 repayAmount, + CToken cTokenCollateral + ) public returns (uint256) { + (uint256 err, ) = liquidateBorrowFresh(liquidator, borrower, repayAmount, cTokenCollateral, false); + return err; + } + + function harnessReduceReservesFresh(uint256 amount) public returns (uint256) { + return _reduceReservesFresh(amount); + } + + function harnessSetReserveFactorFresh(uint256 newReserveFactorMantissa) public returns (uint256) { + return _setReserveFactorFresh(newReserveFactorMantissa); + } + + function harnessSetInterestRateModelFresh(InterestRateModel newInterestRateModel) public returns (uint256) { + return _setInterestRateModelFresh(newInterestRateModel); + } + + function harnessSetInterestRateModel(address newInterestRateModelAddress) public { + interestRateModel = InterestRateModel(newInterestRateModelAddress); + } + + function harnessCallBorrowAllowed(uint256 amount) public returns (uint256) { + return comptroller.borrowAllowed(address(this), msg.sender, amount); + } +} + contract CErc20DelegateScenario is CErc20Delegate { constructor() public {} @@ -563,7 +723,7 @@ contract CErc20DelegateScenarioExtra is CErc20DelegateScenario { } contract CSLPDelegateHarness is CSLPDelegate { - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; @@ -721,7 +881,7 @@ contract CSLPDelegateScenario is CSLPDelegate { } contract CCTokenDelegateHarness is CCTokenDelegate { - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; @@ -886,7 +1046,7 @@ contract CCollateralCapErc20DelegateHarness is CCollateralCapErc20Delegate { event Log(string x, address y); event Log(string x, uint256 y); - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; @@ -1078,7 +1238,7 @@ contract CWrappedNativeDelegateHarness is CWrappedNativeDelegate { event Log(string x, address y); event Log(string x, uint256 y); - uint256 blockNumber = 100000; + uint256 public blockNumber = 100000; uint256 harnessExchangeRate; bool harnessExchangeRateStored; diff --git a/tests/Contracts/EvilToken.sol b/tests/Contracts/EvilToken.sol index a48e538da..52e78da93 100644 --- a/tests/Contracts/EvilToken.sol +++ b/tests/Contracts/EvilToken.sol @@ -1,8 +1,12 @@ pragma solidity ^0.5.16; +import "./ERC20.sol"; import "./FaucetToken.sol"; import "../../contracts/Legacy/CEther.sol"; -import "../../contracts/CCollateralCapErc20.sol"; +import "../../contracts/Legacy/CTokenDeprecated.sol"; +import "../../contracts/CToken.sol"; +import "../../contracts/CErc20.sol"; +import "../../contracts/ComptrollerInterface.sol"; /** * @title The Compound Evil Test Token @@ -73,7 +77,7 @@ contract EvilAccount is RecipientInterface { borrowAmount = _borrowAmount; } - function attack() external payable { + function attackBorrow() external payable { // Mint crEth. CEther(crEth).mint.value(msg.value)(); ComptrollerInterface comptroller = CEther(crEth).comptroller(); @@ -84,7 +88,7 @@ contract EvilAccount is RecipientInterface { comptroller.enterMarkets(markets); // Borrow EvilTransferToken. - require(CCollateralCapErc20(crEvilToken).borrow(borrowAmount) == 0, "first borrow failed"); + require(CErc20(crEvilToken).borrow(borrowAmount) == 0, "first borrow failed"); } function tokensReceived() external { @@ -95,7 +99,55 @@ contract EvilAccount is RecipientInterface { function() external payable {} } +contract EvilAccount2 is RecipientInterface { + address private crWeth; + address private crEvilToken; + address private borrower; + uint256 private repayAmount; + + constructor( + address _crWeth, + address _crEvilToken, + address _borrower, + uint256 _repayAmount + ) public { + crWeth = _crWeth; + crEvilToken = _crEvilToken; + borrower = _borrower; + repayAmount = _repayAmount; + } + + function attackLiquidate() external { + // Make sure the evil account has enough balance to repay. + address evilToken = CErc20(crEvilToken).underlying(); + require(ERC20Base(evilToken).balanceOf(address(this)) > repayAmount, "insufficient balance"); + + // Approve for repayment. + require(ERC20Base(evilToken).approve(crEvilToken, repayAmount) == true, "failed to approve"); + + // Liquidate EvilTransferToken. + require( + CErc20(crEvilToken).liquidateBorrow(borrower, repayAmount, CToken(crWeth)) == 0, + "first liquidate failed" + ); + } + + function tokensReceived() external { + // Make sure the evil account has enough balance to repay. + address weth = CErc20(crWeth).underlying(); + require(ERC20Base(weth).balanceOf(address(this)) > repayAmount, "insufficient balance"); + + // Approve for repayment. + require(ERC20Base(weth).approve(crWeth, repayAmount) == true, "failed to approve"); + + // Liquidate ETH. + CErc20(crWeth).liquidateBorrow(borrower, repayAmount, CToken(crWeth)); + } +} + contract EvilTransferToken is FaucetToken { + bool private attackSwitchOn; + constructor( uint256 _initialAmount, string memory _tokenName, @@ -108,7 +160,9 @@ contract EvilTransferToken is FaucetToken { balanceOf[dst] = balanceOf[dst].add(amount); emit Transfer(msg.sender, dst, amount); - RecipientInterface(dst).tokensReceived(); + if (attackSwitchOn) { + RecipientInterface(dst).tokensReceived(); + } return true; } @@ -121,6 +175,18 @@ contract EvilTransferToken is FaucetToken { balanceOf[dst] = balanceOf[dst].add(amount); allowance[src][msg.sender] = allowance[src][msg.sender].sub(amount); emit Transfer(src, dst, amount); + + if (attackSwitchOn) { + RecipientInterface(src).tokensReceived(); + } return true; } + + function turnSwitchOn() external { + attackSwitchOn = true; + } + + function turnSwitchOff() external { + attackSwitchOn = false; + } } diff --git a/tests/Tokens/attack.js b/tests/Tokens/attack.js index 43d22b88e..ea5204d91 100644 --- a/tests/Tokens/attack.js +++ b/tests/Tokens/attack.js @@ -5,7 +5,8 @@ const { const { makeCToken, makeComptroller, - makeEvilAccount + makeEvilAccount, + makeEvilAccount2 } = require('../Utils/Compound'); const collateralFactor = 0.5, underlyingPrice = 1, mintAmount = 2, borrowAmount = 1; @@ -13,15 +14,21 @@ const collateralFactor = 0.5, underlyingPrice = 1, mintAmount = 2, borrowAmount describe('Attack', function () { let root, accounts; let comptroller; - let cEth, cEvil; + let cEth, cWeth, cEvil; let evilAccount; beforeEach(async () => { [root, ...accounts] = saddle.accounts; - comptroller = await makeComptroller(); + comptroller = await makeComptroller({closeFactor: 0.5}); cEth = await makeCToken({comptroller, kind: 'cether', supportMarket: true, collateralFactor}); + cWeth = await makeCToken({comptroller, kind: 'cerc20', supportMarket: true, collateralFactor, underlyingPrice}); cEvil = await makeCToken({comptroller, kind: 'cevil', supportMarket: true, collateralFactor, underlyingPrice}); evilAccount = await makeEvilAccount({crEth: cEth, crEvil: cEvil, borrowAmount: etherMantissa(borrowAmount)}); + + // Align the block number. + const blockNumber = await call(cEth, 'blockNumber'); + await send(cWeth, 'harnessSetBlockNumber', [blockNumber]); + await send(cEvil, 'harnessSetBlockNumber', [blockNumber]); }); it('reentry borrow attack', async () => { @@ -31,11 +38,77 @@ describe('Attack', function () { // Actually, this attack will emit a Failure event with value (3: COMPTROLLER_REJECTION, 6: BORROW_COMPTROLLER_REJECTION, 4: INSUFFICIENT_LIQUIDITY). // However, somehow it failed to parse the event. - await send(evilAccount, 'attack', [], {value: etherMantissa(mintAmount)}); + await send(cEvil.underlying, 'turnSwitchOn'); + await send(evilAccount, 'attackBorrow', [], {value: etherMantissa(mintAmount)}); // The attack should have no effect. ({1: liquidity, 2: shortfall} = await call(comptroller, 'getAccountLiquidity', [evilAccount._address])); expect(liquidity).toEqualNumber(0); expect(shortfall).toEqualNumber(0); }); + + it('reentry liquidate attack', async () => { + /** + * In this test, a victim supplied 20 WETH (collateral = 10) and borrowed 5 Evil and 5 WETH, which used all of his collateral. + * Next, we changed the price of Evil (1 to 1.1) to make the victim liquidatable. If a successful attack happened, an attacker + * could liquidate 2.5 Evil and 2.5 WETH. It's similiar to bypass the close factor since the victim only have 0.5 shortfall. + * + * In our new CCollateralCapErc20CheckRepay, it could prevent such an attack. If a re-entry liquidation attacked happened, + * it should revert with 'borrower has no shortfall' during the second liquidation. + * + * After that, we could use an EOA to liquidate the victim for 2.5 Evil (or 2.5 WETH). + */ + const victim = accounts[0]; + const liquidator = accounts[1]; + const repayAmount = etherMantissa(2.5); + const evilAccount2 = await makeEvilAccount2({crWeth: cWeth, crEvil: cEvil, borrower: victim, repayAmount: repayAmount}); + + // Supply 20 WETH. + await send(cWeth.underlying, 'harnessSetBalance', [victim, etherMantissa(20)]); + await send(cWeth.underlying, 'approve', [cWeth._address, etherMantissa(20)], {from: victim}); + await send(cWeth, 'mint', [etherMantissa(20)], {from: victim}); + await send(comptroller, 'enterMarkets', [[cWeth._address]], {from: victim}); + + // Borrow 5 WETH and 5 Evil. + await send(cWeth, 'borrow', [etherMantissa(5)], {from: victim}); + await send(cEvil.underlying, 'allocateTo', [cEvil._address, etherMantissa(5)]); + await send(cEvil, 'gulp'); + await send(cEvil, 'borrow', [etherMantissa(5)], {from: victim}); + + // Check account liquidity: no more liquidity and no shortfall at this moment. + ({1: liquidity, 2: shortfall} = await call(comptroller, 'getAccountLiquidity', [victim])); + expect(liquidity).toEqualNumber(0); + expect(shortfall).toEqualNumber(0); + + // Change the Evil price to make the victim could be liqudated. + const newUnderlyingPrice = 1.1; + await send(comptroller.priceOracle, 'setUnderlyingPrice', [cEvil._address, etherMantissa(newUnderlyingPrice)]); + + // Confirm the victim has shortfall. + ({1: liquidity, 2: shortfall} = await call(comptroller, 'getAccountLiquidity', [victim])); + expect(liquidity).toEqualNumber(0); + expect(shortfall).toEqualNumber(etherMantissa(0.5)); + + // Attack the victim through liquidation. + await send(cWeth.underlying, 'harnessSetBalance', [evilAccount2._address, etherMantissa(10)]); + await send(cEvil.underlying, 'allocateTo', [evilAccount2._address, etherMantissa(10)]); + await send(cEvil.underlying, 'turnSwitchOn'); + await expect(send(evilAccount2, 'attackLiquidate')).rejects.toRevert('revert borrower has no shortfall'); + + // The re-entry liquidation attack failed. The victim still has shortfall. + ({1: liquidity, 2: shortfall} = await call(comptroller, 'getAccountLiquidity', [victim])); + expect(liquidity).toEqualNumber(0); + expect(shortfall).toEqualNumber(etherMantissa(0.5)); + + // We use an EOA to liquidate the victim. + await send(cEvil.underlying, 'allocateTo', [liquidator, repayAmount]); + await send(cEvil.underlying, 'approve', [cEvil._address, repayAmount], {from: liquidator}); + await send(cEvil.underlying, 'turnSwitchOff'); + await send(cEvil, 'liquidateBorrow', [victim, repayAmount, cWeth._address], {from: liquidator}); + + // The normal liquidation succeeded. The victim should have no shortfall now. + ({1: liquidity, 2: shortfall} = await call(comptroller, 'getAccountLiquidity', [victim])); + expect(liquidity).toEqualNumber(etherMantissa(0.875)); + expect(shortfall).toEqualNumber(0); + }); }); diff --git a/tests/Utils/Compound.js b/tests/Utils/Compound.js index 6030f36f8..e83e08726 100644 --- a/tests/Utils/Compound.js +++ b/tests/Utils/Compound.js @@ -232,8 +232,8 @@ async function makeCToken(opts = {}) { case 'cevil': underlying = await makeToken({kind: "evil"}); - cDelegatee = await deploy('CErc20DelegateHarness'); - cDelegator = await deploy('CErc20Delegator', + cDelegatee = await deploy('CCollaterlaCapErc20CheckRepayDelegateHarness'); + cDelegator = await deploy('CCollateralCapErc20Delegator', [ underlying._address, comptroller._address, @@ -247,7 +247,8 @@ async function makeCToken(opts = {}) { "0x0" ] ); - cToken = await saddle.getContractAt('CErc20DelegateHarness', cDelegator._address); // XXXS at + cToken = await saddle.getContractAt('CCollaterlaCapErc20CheckRepayDelegateHarness', cDelegator._address); // XXXS at + version = 1; // ccollateralcap's version is 1 break; case 'cerc20': @@ -441,6 +442,14 @@ async function makeEvilAccount(opts = {}) { return await deploy('EvilAccount', [crEth._address, crEvil._address, borrowAmount]); } +async function makeEvilAccount2(opts = {}) { + const crWeth = opts.crWeth || await makeCToken({kind: 'cerc20'}); + const crEvil = opts.crEvil || await makeCToken({kind: 'cevil'}); + const borrower = opts.borrower; + const repayAmount = opts.repayAmount || etherMantissa(1); + return await deploy('EvilAccount2', [crWeth._address, crEvil._address, borrower, repayAmount]); +} + async function preCSLP(underlying) { const sushiToken = await deploy('SushiToken'); const masterChef = await deploy('MasterChef', [sushiToken._address]); @@ -658,6 +667,7 @@ module.exports = { makeCurveSwap, makeLiquidityMining, makeEvilAccount, + makeEvilAccount2, makeCTokenAdmin, balanceOf,