code-423n4 / 2023-12-initcapital-findings

3 stars 3 forks source link

`setPosMode` should not allow changing the mode when the new mode's `canRepay` status is disabled #26

Open c4-bot-2 opened 11 months ago

c4-bot-2 commented 11 months ago

Lines of code

https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L169-L213

Vulnerability details

Impact

In the scenario where the mode's canRepay status is set to false, positions using that mode cannot be repaid and liquidated. However, users are allowed to change their position's mode to one where the canRepay status is currently set to false. This could be exploited when a position owner observes that their position's health is approaching the liquidation threshold, allowing them to prevent liquidation.

Proof of Concept

It can be observed that when setPosMode is called, it check that newModeStatus.canBorrow and currentModeStatus.canRepay is set to true. However, it doesn't the status of newModeStatus.canRepay flag.

https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L203-L204

    function setPosMode(uint _posId, uint16 _mode)
        public
        virtual
        onlyAuthorized(_posId)
        ensurePositionHealth(_posId)
        nonReentrant
    {
        IConfig _config = IConfig(config);
        // get current collaterals in the position
        (address[] memory pools,, address[] memory wLps, uint[][] memory ids,) =
            IPosManager(POS_MANAGER).getPosCollInfo(_posId);
        uint16 currentMode = _getPosMode(_posId);
        ModeStatus memory currentModeStatus = _config.getModeStatus(currentMode);
        ModeStatus memory newModeStatus = _config.getModeStatus(_mode);
        if (pools.length != 0 || wLps.length != 0) {
            _require(newModeStatus.canCollateralize, Errors.COLLATERALIZE_PAUSED);
            _require(currentModeStatus.canDecollateralize, Errors.DECOLLATERALIZE_PAUSED);
        }
        // check that each position collateral belongs to the _mode
        for (uint i; i < pools.length; i = i.uinc()) {
            _require(_config.isAllowedForCollateral(_mode, pools[i]), Errors.INVALID_MODE);
        }
        for (uint i; i < wLps.length; i = i.uinc()) {
            for (uint j; j < ids[i].length; j = j.uinc()) {
                _require(_config.isAllowedForCollateral(_mode, IBaseWrapLp(wLps[i]).lp(ids[i][j])), Errors.INVALID_MODE);
            }
        }
        // get current debts in the position
        uint[] memory shares;
        (pools, shares) = IPosManager(POS_MANAGER).getPosBorrInfo(_posId);
        IRiskManager _riskManager = IRiskManager(riskManager);
        // check that each position debt belongs to the _mode
        for (uint i; i < pools.length; i = i.uinc()) {
            _require(_config.isAllowedForBorrow(_mode, pools[i]), Errors.INVALID_MODE);
            _require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
            _require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
            // update debt on current mode
            _riskManager.updateModeDebtShares(currentMode, pools[i], -shares[i].toInt256());
            // update debt on new mode
            _riskManager.updateModeDebtShares(_mode, pools[i], shares[i].toInt256());
        }
        // update position mode
        IPosManager(POS_MANAGER).updatePosMode(_posId, _mode);
        emit SetPositionMode(_posId, _mode);
    }

As mentioned before, if users see his position's health status is about to reach liquidation threshold and change the mode, this will allow users to prevent their positions from getting liquidated, as both liquidate and liquidateWLp will check the canRepay flag and revert if it's not allowed.

https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L282-L314 https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L317-L353 https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L587-L599

    /// @dev liquidation internal logic
    function _liquidateInternal(uint _posId, address _poolToRepay, uint _repayShares)
        internal
        returns (LiquidateLocalVars memory vars)
    {
        vars.config = IConfig(config);
        vars.mode = _getPosMode(_posId);

        // check position must be unhealthy
        vars.health_e18 = getPosHealthCurrent_e18(_posId);
        _require(vars.health_e18 < ONE_E18, Errors.POSITION_HEALTHY);

>>>     (vars.repayToken, vars.repayAmt) = _repay(vars.config, vars.mode, _posId, _poolToRepay, _repayShares);
    }

https://github.com/code-423n4/2023-12-initcapital/blob/main/contracts/core/InitCore.sol#L530-L551

    function _repay(IConfig _config, uint16 _mode, uint _posId, address _pool, uint _shares)
        internal
        returns (address tokenToRepay, uint amt)
    {
        // check status
>>>     _require(_config.getPoolConfig(_pool).canRepay && _config.getModeStatus(_mode).canRepay, Errors.REPAY_PAUSED);
        // get position debt share
        uint positionDebtShares = IPosManager(POS_MANAGER).getPosDebtShares(_posId, _pool);
        uint sharesToRepay = _shares < positionDebtShares ? _shares : positionDebtShares;
        // get amtToRepay (accrue interest)
        uint amtToRepay = ILendingPool(_pool).debtShareToAmtCurrent(sharesToRepay);
        // take token from msg.sender to pool
        tokenToRepay = ILendingPool(_pool).underlyingToken();
        IERC20(tokenToRepay).safeTransferFrom(msg.sender, _pool, amtToRepay);
        // update debt on the position
        IPosManager(POS_MANAGER).updatePosDebtShares(_posId, _pool, -sharesToRepay.toInt256());
        // call repay on the pool
        amt = ILendingPool(_pool).repay(sharesToRepay);
        // update debt on mode
        IRiskManager(riskManager).updateModeDebtShares(_mode, _pool, -sharesToRepay.toInt256());
        emit Repay(_pool, _posId, msg.sender, _shares, amt);
    }

Tools Used

Manual review

Recommended Mitigation Steps

Add a canRepay check status inside setPosMode; if it is paused, revert the change. Besides that, the canRepay and canBorrow checks don't need to be inside the pools check loop.

    function setPosMode(uint _posId, uint16 _mode)
        public
        virtual
        onlyAuthorized(_posId)
        ensurePositionHealth(_posId)
        nonReentrant
    {
        IConfig _config = IConfig(config);
        // get current collaterals in the position
        (address[] memory pools,, address[] memory wLps, uint[][] memory ids,) =
            IPosManager(POS_MANAGER).getPosCollInfo(_posId);
        uint16 currentMode = _getPosMode(_posId);
        ModeStatus memory currentModeStatus = _config.getModeStatus(currentMode);
        ModeStatus memory newModeStatus = _config.getModeStatus(_mode);
        if (pools.length != 0 || wLps.length != 0) {
            _require(newModeStatus.canCollateralize, Errors.COLLATERALIZE_PAUSED);
            _require(currentModeStatus.canDecollateralize, Errors.DECOLLATERALIZE_PAUSED);
        }
        // check that each position collateral belongs to the _mode
        for (uint i; i < pools.length; i = i.uinc()) {
            _require(_config.isAllowedForCollateral(_mode, pools[i]), Errors.INVALID_MODE);
        }
        for (uint i; i < wLps.length; i = i.uinc()) {
            for (uint j; j < ids[i].length; j = j.uinc()) {
                _require(_config.isAllowedForCollateral(_mode, IBaseWrapLp(wLps[i]).lp(ids[i][j])), Errors.INVALID_MODE);
            }
        }
        // get current debts in the position
        uint[] memory shares;
        (pools, shares) = IPosManager(POS_MANAGER).getPosBorrInfo(_posId);
        IRiskManager _riskManager = IRiskManager(riskManager);
        // check that each position debt belongs to the _mode
+      _require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
+      _require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
+      _require(newModeStatus.canRepay, Errors.REPAY_PAUSED);
        for (uint i; i < pools.length; i = i.uinc()) {
            _require(_config.isAllowedForBorrow(_mode, pools[i]), Errors.INVALID_MODE);
-            _require(newModeStatus.canBorrow, Errors.BORROW_PAUSED);
-            _require(currentModeStatus.canRepay, Errors.REPAY_PAUSED);
            // update debt on current mode
            _riskManager.updateModeDebtShares(currentMode, pools[i], -shares[i].toInt256());
            // update debt on new mode
            _riskManager.updateModeDebtShares(_mode, pools[i], shares[i].toInt256());
        }
        // update position mode
        IPosManager(POS_MANAGER).updatePosMode(_posId, _mode);
        emit SetPositionMode(_posId, _mode);
    }

Assessed type

Invalid Validation

c4-judge commented 10 months ago

hansfriese marked the issue as primary issue

c4-sponsor commented 10 months ago

fez-init (sponsor) confirmed

c4-judge commented 10 months ago

hansfriese marked the issue as satisfactory

c4-judge commented 10 months ago

hansfriese marked the issue as selected for report