pragma solidity 0.6.12;
pragma experimental ABIEncoderV2;

interface IComptroller {

    function enterMarkets(address[] calldata _cTokens) external returns (uint[] memory);

    function exitMarket(address _cToken) external returns (uint);

    function getAssetsIn(address _account) external view returns (address[] memory);

    function getAccountLiquidity(address _account) external view returns (uint, uint, uint);

    function checkMembership(address account, ICToken cToken) external view returns (bool);

}

interface ICToken {

    function comptroller() external view returns (address);

    function underlying() external view returns (address);

    function symbol() external view returns (string memory);

    function exchangeRateCurrent() external returns (uint256);

    function exchangeRateStored() external view returns (uint256);

    function balanceOf(address _account) external view returns (uint256);

    function borrowBalanceCurrent(address _account) external returns (uint256);

    function borrowBalanceStored(address _account) external view returns (uint256);

}

interface ERC20 {

    function totalSupply() external view returns (uint);

    function decimals() external view returns (uint);

    function balanceOf(address tokenOwner) external view returns (uint balance);

    function allowance(address tokenOwner, address spender) external view returns (uint remaining);

    function transfer(address to, uint tokens) external returns (bool success);

    function approve(address spender, uint tokens) external returns (bool success);

    function transferFrom(address from, address to, uint tokens) external returns (bool success);

}

interface ICompoundRegistry {

    function addCToken(address _underlying, address _cToken) external;



    function removeCToken(address _underlying) external;



    function getCToken(address _underlying) external view returns (address);



    function listUnderlyings() external view returns (address[] memory);

}

interface IFeature {



    enum OwnerSignature {

        Anyone,             

        Required,           

        Optional,           

        Disallowed          

    }



    

    function recoverToken(address _token) external;



    

    function init(address _wallet) external;



    

    function isFeatureAuthorisedInVersionManager(address _wallet, address _feature) external view returns (bool);



    

    function getRequiredSignatures(address _wallet, bytes calldata _data) external view returns (uint256, OwnerSignature);



    

    function getStaticCallSignatures() external view returns (bytes4[] memory);

}

interface ILimitStorage {



    struct Limit {

        

        uint128 current;

        

        uint128 pending;

        

        uint64 changeAfter;

    }



    struct DailySpent {

        

        uint128 alreadySpent;

        

        uint64 periodEnd;

    }



    function setLimit(address _wallet, Limit memory _limit) external;



    function getLimit(address _wallet) external view returns (Limit memory _limit);



    function setDailySpent(address _wallet, DailySpent memory _dailySpent) external;



    function getDailySpent(address _wallet) external view returns (DailySpent memory _dailySpent);



    function setLimitAndDailySpent(address _wallet, Limit memory _limit, DailySpent memory _dailySpent) external;



    function getLimitAndDailySpent(address _wallet) external view returns (Limit memory _limit, DailySpent memory _dailySpent);

}

interface ILockStorage {

    function isLocked(address _wallet) external view returns (bool);



    function getLock(address _wallet) external view returns (uint256);



    function getLocker(address _wallet) external view returns (address);



    function setLock(address _wallet, address _locker, uint256 _releaseAfter) external;

}

interface IModuleRegistry {

    function registerModule(address _module, bytes32 _name) external;



    function deregisterModule(address _module) external;



    function registerUpgrader(address _upgrader, bytes32 _name) external;



    function deregisterUpgrader(address _upgrader) external;



    function recoverToken(address _token) external;



    function moduleInfo(address _module) external view returns (bytes32);



    function upgraderInfo(address _upgrader) external view returns (bytes32);



    function isRegisteredModule(address _module) external view returns (bool);



    function isRegisteredModule(address[] calldata _modules) external view returns (bool);



    function isRegisteredUpgrader(address _upgrader) external view returns (bool);

}

interface IVersionManager {

    

    function isFeatureAuthorised(address _wallet, address _feature) external view returns (bool);



    

    function checkAuthorisedFeatureAndInvokeWallet(

        address _wallet,

        address _to,

        uint256 _value,

        bytes calldata _data

    ) external returns (bytes memory _res);



    



    

    function setOwner(address _wallet, address _newOwner) external;



    

    function invokeStorage(address _wallet, address _storage, bytes calldata _data) external;



    

    function upgradeWallet(address _wallet, uint256 _toVersion) external;

 

}

interface IWallet {

    

    function owner() external view returns (address);



    

    function modules() external view returns (uint);



    

    function setOwner(address _newOwner) external;



    

    function authorised(address _module) external view returns (bool);



    

    function enabled(bytes4 _sig) external view returns (address);



    

    function authoriseModule(address _module, bool _value) external;



    

    function enableStaticCall(address _module, bytes4 _method) external;

}

library SafeMath {

    

    function add(uint256 a, uint256 b) internal pure returns (uint256) {

        uint256 c = a + b;

        require(c >= a, "SafeMath: addition overflow");



        return c;

    }



    

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {

        return sub(a, b, "SafeMath: subtraction overflow");

    }



    

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {

        require(b <= a, errorMessage);

        uint256 c = a - b;



        return c;

    }



    

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {

        

        

        

        if (a == 0) {

            return 0;

        }



        uint256 c = a * b;

        require(c / a == b, "SafeMath: multiplication overflow");



        return c;

    }



    

    function div(uint256 a, uint256 b) internal pure returns (uint256) {

        return div(a, b, "SafeMath: division by zero");

    }



    

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {

        

        require(b > 0, errorMessage);

        uint256 c = a / b;

        



        return c;

    }



    

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {

        return mod(a, b, "SafeMath: modulo by zero");

    }



    

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {

        require(b != 0, errorMessage);

        return a % b;

    }

}

contract BaseFeature is IFeature {



    

    bytes constant internal EMPTY_BYTES = "";

    

    address constant internal ETH_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    

    ILockStorage internal lockStorage;

    

    IVersionManager internal versionManager;



    event FeatureCreated(bytes32 name);



    

    modifier onlyWhenUnlocked(address _wallet) {

        require(!lockStorage.isLocked(_wallet), "BF: wallet locked");

        _;

    }



    

    modifier onlyVersionManager() {

        require(msg.sender == address(versionManager), "BF: caller must be VersionManager");

        _;

    }



    

    modifier onlyWalletOwner(address _wallet) {

        require(isOwner(_wallet, msg.sender), "BF: must be wallet owner");

        _;

    }



    

    modifier onlyWalletFeature(address _wallet) {

        require(versionManager.isFeatureAuthorised(_wallet, msg.sender), "BF: must be a wallet feature");

        _;

    }



    

    modifier onlyWalletOwnerOrFeature(address _wallet) {

        

        verifyOwnerOrAuthorisedFeature(_wallet, msg.sender);

        _;

    }



    constructor(

        ILockStorage _lockStorage,

        IVersionManager _versionManager,

        bytes32 _name

    ) public {

        lockStorage = _lockStorage;

        versionManager = _versionManager;

        emit FeatureCreated(_name);

    }



    

    function recoverToken(address _token) external virtual override {

        uint total = ERC20(_token).balanceOf(address(this));

        _token.call(abi.encodeWithSelector(ERC20(_token).transfer.selector, address(versionManager), total));

    }



    

    function init(address _wallet) external virtual override  {}



    

    function getRequiredSignatures(address, bytes calldata) external virtual view override returns (uint256, OwnerSignature) {

        revert("BF: disabled method");

    }



    

    function getStaticCallSignatures() external virtual override view returns (bytes4[] memory _sigs) {}



    

    function isFeatureAuthorisedInVersionManager(address _wallet, address _feature) public override view returns (bool) {

        return versionManager.isFeatureAuthorised(_wallet, _feature);

    }



    

    function verifyData(address _wallet, bytes calldata _data) internal pure returns (bool) {

        require(_data.length >= 36, "RM: Invalid dataWallet");

        address dataWallet = abi.decode(_data[4:], (address));

        return dataWallet == _wallet;

    }

    

     

    function isOwner(address _wallet, address _addr) internal view returns (bool) {

        return IWallet(_wallet).owner() == _addr;

    }



    

    function verifyOwnerOrAuthorisedFeature(address _wallet, address _sender) internal view {

        require(isFeatureAuthorisedInVersionManager(_wallet, _sender) || isOwner(_wallet, _sender), "BF: must be owner or feature");

    }



    

    function invokeWallet(address _wallet, address _to, uint256 _value, bytes memory _data)

        internal

        returns (bytes memory _res) 

    {

        _res = versionManager.checkAuthorisedFeatureAndInvokeWallet(_wallet, _to, _value, _data);

    }



}

contract CompoundManager is BaseFeature{



    bytes32 constant NAME = "CompoundManager";



    

    IComptroller public comptroller;

    

    ICompoundRegistry public compoundRegistry;



    event InvestmentAdded(address indexed _wallet, address _token, uint256 _invested, uint256 _period);

    event InvestmentRemoved(address indexed _wallet, address _token, uint256 _fraction);

    event LoanOpened(

        address indexed _wallet,

        bytes32 indexed _loanId,

        address _collateral,

        uint256 _collateralAmount,

        address _debtToken,

        uint256 _debtAmount);

    event LoanClosed(address indexed _wallet, bytes32 indexed _loanId);

    event CollateralAdded(address indexed _wallet, bytes32 indexed _loanId, address _collateral, uint256 _collateralAmount);

    event CollateralRemoved(address indexed _wallet, bytes32 indexed _loanId, address _collateral, uint256 _collateralAmount);

    event DebtAdded(address indexed _wallet, bytes32 indexed _loanId, address _debtToken, uint256 _debtAmount);

    event DebtRemoved(address indexed _wallet, bytes32 indexed _loanId, address _debtToken, uint256 _debtAmount);



    using SafeMath for uint256;



    constructor(

        ILockStorage _lockStorage,

        IComptroller _comptroller,

        ICompoundRegistry _compoundRegistry,

        IVersionManager _versionManager

    )

        BaseFeature(_lockStorage, _versionManager, NAME)

        public

    {

        comptroller = _comptroller;

        compoundRegistry = _compoundRegistry;

    }



    

    function getRequiredSignatures(address, bytes calldata) external view override returns (uint256, OwnerSignature) {

        return (1, OwnerSignature.Required);

    }



    



    

    function openLoan(

        address _wallet,

        address _collateral,

        uint256 _collateralAmount,

        address _debtToken,

        uint256 _debtAmount

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

        returns (bytes32 _loanId)

    {

        address[] memory markets = new address[](2);

        markets[0] = compoundRegistry.getCToken(_collateral);

        markets[1] = compoundRegistry.getCToken(_debtToken);

        invokeWallet(_wallet, address(comptroller), 0, abi.encodeWithSignature("enterMarkets(address[])", markets));

        mint(_wallet, markets[0], _collateral, _collateralAmount);

        borrow(_wallet, _debtToken, markets[1], _debtAmount);

        emit LoanOpened(_wallet, _loanId, _collateral, _collateralAmount, _debtToken, _debtAmount);

    }



    

    function closeLoan(

        address _wallet,

        bytes32 _loanId

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        address[] memory markets = comptroller.getAssetsIn(_wallet);

        for (uint i = 0; i < markets.length; i++) {

            address cToken = markets[i];

            uint debt = ICToken(cToken).borrowBalanceCurrent(_wallet);

            if (debt > 0) {

                repayBorrow(_wallet, cToken, debt);

                uint collateral = ICToken(cToken).balanceOf(_wallet);

                if (collateral == 0) {

                    invokeWallet(

                        _wallet,

                        address(comptroller),

                        0,

                        abi.encodeWithSignature("exitMarket(address)", address(cToken))

                    );

                }

            }

        }

        emit LoanClosed(_wallet, _loanId);

    }



    

    function addCollateral(

        address _wallet,

        bytes32 _loanId,

        address _collateral,

        uint256 _collateralAmount

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        address cToken = compoundRegistry.getCToken(_collateral);

        enterMarketIfNeeded(_wallet, cToken, address(comptroller));

        mint(_wallet, cToken, _collateral, _collateralAmount);

        emit CollateralAdded(_wallet, _loanId, _collateral, _collateralAmount);

    }



    

    function removeCollateral(

        address _wallet,

        bytes32 _loanId,

        address _collateral,

        uint256 _collateralAmount

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        address cToken = compoundRegistry.getCToken(_collateral);

        redeemUnderlying(_wallet, cToken, _collateralAmount);

        exitMarketIfNeeded(_wallet, cToken, address(comptroller));

        emit CollateralRemoved(_wallet, _loanId, _collateral, _collateralAmount);

    }



    

    function addDebt(

        address _wallet,

        bytes32 _loanId,

        address _debtToken,

        uint256 _debtAmount

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        address dToken = compoundRegistry.getCToken(_debtToken);

        enterMarketIfNeeded(_wallet, dToken, address(comptroller));

        borrow(_wallet, _debtToken, dToken, _debtAmount);

        emit DebtAdded(_wallet, _loanId, _debtToken, _debtAmount);

    }



    

    function removeDebt(

        address _wallet,

        bytes32 _loanId,

        address _debtToken,

        uint256 _debtAmount

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        address dToken = compoundRegistry.getCToken(_debtToken);

        repayBorrow(_wallet, dToken, _debtAmount);

        exitMarketIfNeeded(_wallet, dToken, address(comptroller));

        emit DebtRemoved(_wallet, _loanId, _debtToken, _debtAmount);

    }



    

    function getLoan(

        address _wallet,

        bytes32 

    )

        external

        view

        returns (uint8 _status, uint256 _ethValue)

    {

        (uint error, uint liquidity, uint shortfall) = comptroller.getAccountLiquidity(_wallet);

        require(error == 0, "CM: failed to get account liquidity");

        if (liquidity > 0) {

            return (1, liquidity);

        }

        if (shortfall > 0) {

            return (2, shortfall);

        }

        return (0,0);

    }



    



    

    function addInvestment(

        address _wallet,

        address _token,

        uint256 _amount,

        uint256 _period

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

        returns (uint256 _invested)

    {

        address cToken = compoundRegistry.getCToken(_token);

        mint(_wallet, cToken, _token, _amount);

        _invested = _amount;

        emit InvestmentAdded(_wallet, _token, _amount, _period);

    }



    

    function removeInvestment(

        address _wallet,

        address _token,

        uint256 _fraction

    )

        external

        onlyWalletOwnerOrFeature(_wallet)

        onlyWhenUnlocked(_wallet)

    {

        require(_fraction <= 10000, "CM: invalid fraction value");

        address cToken = compoundRegistry.getCToken(_token);

        uint shares = ICToken(cToken).balanceOf(_wallet);

        redeem(_wallet, cToken, shares.mul(_fraction).div(10000));

        emit InvestmentRemoved(_wallet, _token, _fraction);

    }



    

    function getInvestment(

        address _wallet,

        address _token

    )

        external

        view

        returns (uint256 _tokenValue, uint256 _periodEnd)

    {

        address cToken = compoundRegistry.getCToken(_token);

        uint amount = ICToken(cToken).balanceOf(_wallet);

        uint exchangeRateMantissa = ICToken(cToken).exchangeRateStored();

        _tokenValue = amount.mul(exchangeRateMantissa).div(10 ** 18);

        _periodEnd = 0;

    }



    



    

    function mint(address _wallet, address _cToken, address _token, uint256 _amount) internal {

        require(_cToken != address(0), "CM: No market for target token");

        require(_amount > 0, "CM: amount cannot be 0");

        uint256 initialCTokenAmount = ERC20(_cToken).balanceOf(_wallet);

        if (_token == ETH_TOKEN) {

            invokeWallet(_wallet, _cToken, _amount, abi.encodeWithSignature("mint()"));

        } else {

            invokeWallet(_wallet, _token, 0, abi.encodeWithSignature("approve(address,uint256)", _cToken, _amount));

            invokeWallet(_wallet, _cToken, 0, abi.encodeWithSignature("mint(uint256)", _amount));

        }

        require(ERC20(_cToken).balanceOf(_wallet) > initialCTokenAmount, "CM: mint failed");

    }



    

    function redeem(address _wallet, address _cToken, uint256 _amount) internal {

        

        

        

        

        require(_amount > 0, "CM: amount cannot be 0");

        uint256 initialCTokenAmount = ERC20(_cToken).balanceOf(_wallet);

        invokeWallet(_wallet, _cToken, 0, abi.encodeWithSignature("redeem(uint256)", _amount));

        require(ERC20(_cToken).balanceOf(_wallet) < initialCTokenAmount, "CM: redeem failed");

    }



    

    function redeemUnderlying(address _wallet, address _cToken, uint256 _amount) internal {

        require(_cToken != address(0), "CM: No market for target token");

        require(_amount > 0, "CM: amount cannot be 0");

        uint256 initialCTokenAmount = ERC20(_cToken).balanceOf(_wallet);

        invokeWallet(_wallet, _cToken, 0, abi.encodeWithSignature("redeemUnderlying(uint256)", _amount));

        require(ERC20(_cToken).balanceOf(_wallet) < initialCTokenAmount, "CM: redeemUnderlying failed");

    }



    

    function borrow(address _wallet, address _token, address _cToken, uint256 _amount) internal {

        require(_cToken != address(0), "CM: No market for target token");

        require(_amount > 0, "CM: amount cannot be 0");

        uint256 initialTokenAmount = _token == ETH_TOKEN ? _wallet.balance : ERC20(_token).balanceOf(_wallet);

        invokeWallet(_wallet, _cToken, 0, abi.encodeWithSignature("borrow(uint256)", _amount));

        uint256 finalTokenAmount = _token == ETH_TOKEN ? _wallet.balance : ERC20(_token).balanceOf(_wallet);

        require(finalTokenAmount > initialTokenAmount, "CM: borrow failed");

    }



    

    function repayBorrow(address _wallet, address _cToken, uint256 _amount) internal {

        require(_cToken != address(0), "CM: No market for target token");

        require(_amount > 0, "CM: amount cannot be 0");

        string memory symbol = ICToken(_cToken).symbol();

        uint256 initialTokenAmount;

        uint256 finalTokenAmount;

        if (keccak256(abi.encodePacked(symbol)) == keccak256(abi.encodePacked("cETH"))) {

            initialTokenAmount = _wallet.balance;

            invokeWallet(_wallet, _cToken, _amount, abi.encodeWithSignature("repayBorrow()"));

            finalTokenAmount = _wallet.balance;

        } else {

            address token = ICToken(_cToken).underlying();

            initialTokenAmount = ERC20(token).balanceOf(_wallet);

            invokeWallet(_wallet, token, 0, abi.encodeWithSignature("approve(address,uint256)", _cToken, _amount));

            invokeWallet(_wallet, _cToken, 0, abi.encodeWithSignature("repayBorrow(uint256)", _amount));

            finalTokenAmount = ERC20(token).balanceOf(_wallet);

        }

        require(finalTokenAmount < initialTokenAmount, "CM: repayBorrow failed");

    }



    

    function enterMarketIfNeeded(address _wallet, address _cToken, address _comptroller) internal {

        bool isEntered = IComptroller(_comptroller).checkMembership(_wallet, ICToken(_cToken));

        if (!isEntered) {

            address[] memory market = new address[](1);

            market[0] = _cToken;

            invokeWallet(_wallet, _comptroller, 0, abi.encodeWithSignature("enterMarkets(address[])", market));

        }

    }



    

    function exitMarketIfNeeded(address _wallet, address _cToken, address _comptroller) internal {

        uint collateral = ICToken(_cToken).balanceOf(_wallet);

        uint debt = ICToken(_cToken).borrowBalanceStored(_wallet);

        if (collateral == 0 && debt == 0) {

            invokeWallet(_wallet, _comptroller, 0, abi.encodeWithSignature("exitMarket(address)", _cToken));

        }

    }

}