diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 05bcbe7c9..74f6e3a96 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -12,7 +12,7 @@ import "./Governance/Comp.sol"; * @title Compound's Comptroller Contract * @author Compound */ -contract Comptroller is ComptrollerV5Storage, ComptrollerInterface, ComptrollerErrorReporter, ExponentialNoError { +contract Comptroller is ComptrollerV6Storage, ComptrollerInterface, ComptrollerErrorReporter, ExponentialNoError { /// @notice Emitted when an admin supports a market event MarketListed(CToken cToken); @@ -64,6 +64,9 @@ contract Comptroller is ComptrollerV5Storage, ComptrollerInterface, ComptrollerE /// @notice Emitted when COMP is granted by admin event CompGranted(address recipient, uint amount); + /// @notice Emitted when B.Protocol is changed + event NewBProtocol(address indexed cToken, address oldBProtocol, address newBProtocol); + /// @notice The initial COMP index for a market uint224 public constant compInitialIndex = 1e36; @@ -466,8 +469,6 @@ contract Comptroller is ComptrollerV5Storage, ComptrollerInterface, ComptrollerE address liquidator, address borrower, uint repayAmount) external returns (uint) { - // Shh - currently unused - liquidator; if (!markets[cTokenBorrowed].isListed || !markets[cTokenCollateral].isListed) { return uint(Error.MARKET_NOT_LISTED); @@ -495,6 +496,13 @@ contract Comptroller is ComptrollerV5Storage, ComptrollerInterface, ComptrollerE return uint(Error.TOO_MUCH_REPAY); } } + + /* Only B.Protocol can liquidate */ + address bLiquidator = bprotocol[address(cTokenBorrowed)]; + if(bLiquidator != address(0) && IBProtocol(bLiquidator).canLiquidate(cTokenBorrowed, cTokenCollateral, repayAmount)) { + require(liquidator == bLiquidator, "only B.Protocol can liquidate"); + } + return uint(Error.NO_ERROR); } @@ -1317,6 +1325,15 @@ contract Comptroller is ComptrollerV5Storage, ComptrollerInterface, ComptrollerE emit ContributorCompSpeedUpdated(contributor, compSpeed); } + function _setBProtocol(address cToken, address newBProtocol) public returns (uint) { + require(adminOrInitializing(), "only admin can set B.Protocol"); + + emit NewBProtocol(cToken, bprotocol[cToken], newBProtocol); + bprotocol[cToken] = newBProtocol; + + return uint(Error.NO_ERROR); + } + /** * @notice Return all of the markets * @dev The automatic getter may be used to access an individual market. diff --git a/contracts/ComptrollerStorage.sol b/contracts/ComptrollerStorage.sol index 7f0a7e674..b99aff697 100644 --- a/contracts/ComptrollerStorage.sol +++ b/contracts/ComptrollerStorage.sol @@ -2,6 +2,7 @@ pragma solidity ^0.5.16; import "./CToken.sol"; import "./PriceOracle.sol"; +import "./IBProtocol.sol"; contract UnitrollerAdminStorage { /** @@ -143,3 +144,8 @@ contract ComptrollerV5Storage is ComptrollerV4Storage { /// @notice Last block at which a contributor's COMP rewards have been allocated mapping(address => uint) public lastContributorBlock; } + +contract ComptrollerV6Storage is ComptrollerV5Storage { + /// @notice CToken => IBProtocol (per CToken debt) + mapping(address => address) public bprotocol; +} \ No newline at end of file diff --git a/contracts/IBProtocol.sol b/contracts/IBProtocol.sol new file mode 100644 index 000000000..d4de506e9 --- /dev/null +++ b/contracts/IBProtocol.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.16; + +interface IBProtocol { + function canLiquidate( + address cTokenBorrowed, + address cTokenCollateral, + uint repayAmount + ) + external + view + returns(bool); +} + + diff --git a/tests/Contracts/MockBProtocol.sol b/tests/Contracts/MockBProtocol.sol new file mode 100644 index 000000000..96a58795d --- /dev/null +++ b/tests/Contracts/MockBProtocol.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.5.16; + +contract MockBProtocol { + bool val; + + function setVal(bool _val) external { + val = _val; + } + + function canLiquidate( + address /* cTokenBorrowed */, + address /* cTokenCollateral */, + uint /* repayAmount */ + ) + external + view + returns(bool) { + return val; + } +} + diff --git a/tests/Tokens/liquidateTest.js b/tests/Tokens/liquidateTest.js index 15f0657d8..240ce1f22 100644 --- a/tests/Tokens/liquidateTest.js +++ b/tests/Tokens/liquidateTest.js @@ -312,3 +312,81 @@ describe('Comptroller', () => { await expect(send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount * 2])).rejects.toRevert('revert Can not repay more than the total borrow'); }); }) + +describe('B.Protocol', () => { + it('set B.Protocol', async () => { + let [root, liquidator, borrower] = saddle.accounts; + + const bprotocol1 = await deploy('MockBProtocol', []); + const bprotocol2 = await deploy('MockBProtocol', []); + + const cTokenCollat = await makeCToken({supportMarket: true, underlyingPrice: 1, collateralFactor: .5}); + const cTokenBorrow = await makeCToken({supportMarket: true, underlyingPrice: 1, comptroller: cTokenCollat.comptroller}); + const comptroller = cTokenCollat.comptroller; + + expect(await send(comptroller, '_setBProtocol', [cTokenBorrow._address, bprotocol1._address])).toSucceed(); + await expect(send(comptroller, '_setBProtocol', [cTokenBorrow._address, bprotocol2._address], {from: borrower})).rejects.toRevert('revert only admin can set B.Protocol'); + }); + + it('liquidateBorrowAllowed', async () => { + // setup liquidation state + let [root, liquidator, borrower] = saddle.accounts; + let collatAmount = 10; + let borrowAmount = 2; + const cTokenCollat = await makeCToken({supportMarket: true, underlyingPrice: 1, collateralFactor: .5}); + const cTokenBorrow = await makeCToken({supportMarket: true, underlyingPrice: 1, comptroller: cTokenCollat.comptroller}); + const comptroller = cTokenCollat.comptroller; + + // borrow some tokens + await send(cTokenCollat.underlying, 'harnessSetBalance', [borrower, collatAmount]); + await send(cTokenCollat.underlying, 'approve', [cTokenCollat._address, collatAmount], {from: borrower}); + await send(cTokenBorrow.underlying, 'harnessSetBalance', [cTokenBorrow._address, collatAmount]); + await send(cTokenBorrow, 'harnessSetTotalSupply', [collatAmount * 10]); + await send(cTokenBorrow, 'harnessSetExchangeRate', [etherExp(1)]); + expect(await enterMarkets([cTokenCollat], borrower)).toSucceed(); + expect(await send(cTokenCollat, 'mint', [collatAmount], {from: borrower})).toSucceed(); + expect(await send(cTokenBorrow, 'borrow', [borrowAmount], {from: borrower})).toSucceed(); + + // show the account is healthy + expect(await call(comptroller, 'isDeprecated', [cTokenBorrow._address])).toEqual(false); + expect(await call(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).toHaveTrollError('INSUFFICIENT_SHORTFALL'); + + // show deprecating a market works + expect(await send(comptroller, '_setCollateralFactor', [cTokenBorrow._address, 0])).toSucceed(); + expect(await send(comptroller, '_setBorrowPaused', [cTokenBorrow._address, true])).toSucceed(); + expect(await send(cTokenBorrow, '_setReserveFactor', [etherMantissa(1)])).toSucceed(); + + expect(await call(comptroller, 'isDeprecated', [cTokenBorrow._address])).toEqual(true); + + // show deprecated markets can be liquidated even if healthy + expect(await send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).toSucceed(); + + // with b.protocol + const bprotocol1 = await deploy('MockBProtocol', []); + + // set b.protocol for that token + expect(await send(comptroller, '_setBProtocol', [cTokenBorrow._address, bprotocol1._address])).toSucceed(); + + // check that other users can liquidate, because can liquidate returns false + expect(await send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).toSucceed(); + + // set can liquidate to true + expect(await send(bprotocol1, 'setVal', [true])).toSucceed(); + + // check that other users cannot liquidate + await expect(send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).rejects.toRevert('revert only B.Protocol can liquidate'); + + // check that bprotocol can liquidate + expect(await send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, bprotocol1._address, borrower, borrowAmount])).toSucceed(); + + // reset bprotocol and show now everyone can liquidate + expect(await send(comptroller, '_setBProtocol', [cTokenBorrow._address, "0x0000000000000000000000000000000000000000"])).toSucceed(); + expect(await send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).toSucceed(); + + // set another b.protocol to a different token and show it does not have an affect on the other ctoken + const bprotocol2 = await deploy('MockBProtocol', []); + expect(await send(bprotocol2, 'setVal', [true])).toSucceed(); + expect(await send(comptroller, '_setBProtocol', [cTokenCollat._address, bprotocol2._address])).toSucceed(); + expect(await send(comptroller, 'liquidateBorrowAllowed', [cTokenBorrow._address, cTokenCollat._address, liquidator, borrower, borrowAmount])).toSucceed(); + }); +})