HIGH severity: Discrepancy between health calculation and slashable collateral computation. The protocol exhibits a critical discrepancy between how an agent's health factor is calculated and how slashable collateral is determined. This inconsistency arises because the health factor evaluation uses all current collateral, including recent deposits, while the slashing mechanism only conside...

Vulnerable code:
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { Access } from "../access/Access.sol";

import { IDelegation } from "../interfaces/IDelegation.sol";
import { INetworkMiddleware } from "../interfaces/INetworkMiddleware.sol";

import { IRestakerRewardReceiver } from "../interfaces/IRestakerRewardReceiver.sol";

import { DelegationStorageUtils } from "../storage/DelegationStorageUtils.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

/// @title Cap Delegation Contract
/// @author Cap Labs
/// @notice This contract manages delegation and slashing.
contract Delegation is IDelegation, UUPSUpgradeable, Access, DelegationStorageUtils {
    using SafeERC20 for IERC20;

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /// @notice Initialize the contract
    /// @param _accessControl Access control address
    /// @param _oracle Oracle address
    /// @param _epochDuration Epoch duration in seconds
    function initialize(address _accessControl, address _oracle, uint256 _epochDuration) external initializer {
        __Access_init(_accessControl);
        __UUPSUpgradeable_init();
        DelegationStorage storage $ = getDelegationStorage();
        $.oracle = _oracle;
        $.epochDuration = _epochDuration;
    }

    /// @notice How much global delegation we have in the system
    /// @return delegation Delegation in USD
    function globalDelegation() external view returns (uint256 delegation) {
        DelegationStorage storage $ = getDelegationStorage();
        for (uint i; i < $.agents.length; ++i) {
            delegation += coverage($.agents[i]);
        }
    }

    /// @notice Get the epoch duration
    /// @return duration Epoch duration in seconds
    function epochDuration() external view returns (uint256 duration) {
        DelegationStorage storage $ = getDelegationStorage();
        duration = $.epochDuration;
    }

    /// @notice Get the current epoch
    /// @return currentEpoch Current epoch
    function epoch() public view returns (uint256 currentEpoch) {
        DelegationStorage storage $ = getDelegationStorage();
        currentEpoch = block.timestamp / $.epochDuration;
    }

    /// @notice Get the timestamp that is most recent between the last borrow and the epoch -1
    /// @param _agent The agent address
    /// @return _slashTimestamp Timestamp that is most recent between the last borrow and the epoch -1
    function slashTimestamp(address _agent) public view returns (uint48 _slashTimestamp) {
        DelegationStorage storage $ = getDelegationStorage();
        _slashTimestamp = uint48(Math.max((epoch() - 1) * $.epochDuration, $.agentData[_agent].lastBorrow));
    }

    /// @notice How much delegation and agent has available to back their borrows
    /// @param _agent The agent address
    /// @return delegation Amount in USD (8 decimals) that a agent has provided as delegation from the delegators
    function coverage(address _agent) public view returns (uint256 delegation) {
        DelegationStorage storage $ = getDelegationStorage();
        for (uint i; i < $.networks[_agent].length; ++i) {
            delegation += coverageByNetwork(_agent, $.networks[_agent][i]);
        }
    }

    /// @notice How much slashable coverage an agent has available to back their borrows
    /// @param _agent The agent address
    /// @return _slashableCollateral Amount in USD (8 decimals) that a agent has provided as slashable collateral from the delegators
    function slashableCollateral(address _agent) public view returns (uint256 _slashableCollateral) {
        DelegationStorage storage $ = getDelegationStorage();
        uint48 _slashTimestamp = slashTimestamp(_agent);
        for (uint i; i < $.networks[_agent].length; ++i) {
            _slashableCollateral +=
                INetworkMiddleware($.networks[_agent][i]).slashableCollateral(_agent, _slashTimestamp);
        }
    }

    /// @notice How much delegation and agent has available to back their borrows
    /// @param _agent The agent addres
    /// @param _network The network covering the agent
    /// @return delegation Amount in USD that a agent has as delegation from the networks, encoded with 8 decimals
    function coverageByNetwork(address _agent, address _network) public view returns (uint256 delegation) {
        delegation = INetworkMiddleware(_network).coverage(_agent);
    }

    /// @notice Slashable collateral of an agent by a specific network
    /// @param _agent Agent address
    /// @param _network Network address
    /// @return _slashableCollateral Slashable collateral amount in USD (8 decimals)
    function slashableCollateralByNetwork(address _agent, address _network)
        public
        view
        returns (uint256 _slashableCollateral)
    {
        uint48 _slashTimestamp = slashTimestamp(_agent);
        _slashableCollateral = INetworkMiddleware(_network).slashableCollateral(_agent, _slashTimestamp);
    }

    /// @notice Fetch active network addresses
    /// @param _agent Agent address
    /// @return networkAddresses network addresses
    function networks(address _agent) external view returns (address[] memory networkAddresses) {
        networkAddresses = getDelegationStorage().networks[_agent];
    }

    /// @notice Fetch active agent addresses
    /// @return agentAddresses Agent addresses
    function agents() external view returns (address[] memory agentAddresses) {
        agentAddresses = getDelegationStorage().agents;
    }

    /// @notice The LTV of a specific agent
    /// @param _agent Agent who we are querying
    /// @return currentLtv Loan to value ratio of the agent
    function ltv(address _agent) external view returns (uint256 currentLtv) {
        currentLtv = getDelegationStorage().agentData[_agent].ltv;
    }

    /// @notice Liquidation threshold of the agent
    /// @param _agent Agent who we are querying
    /// @return lt Liquidation threshold of the agent
    function liquidationThreshold(address _agent) external view returns (uint256 lt) {
        lt = getDelegationStorage().agentData[_agent].liquidationThreshold;
    }

    /// @notice The slash function. Calls the underlying networks to slash the delegated capital
    /// @dev Called only by the lender during liquidation
    /// @param _agent The agent who is unhealthy
    /// @param _liquidator The liquidator who receives the funds
    /// @param _amount The USD value of the delegation needed to cover the debt
    function slash(address _agent, address _liquidator, uint256 _amount) external checkAccess(this.slash.selector) {
        DelegationStorage storage $ = getDelegationStorage();
        uint48 _slashTimestamp = slashTimestamp(_agent);

        // Calculate each network's proportion of total delegation
        for (uint i; i < $.networks[_agent].length; ++i) {
            address network = $.networks[_agent][i];
            uint256 networkSlashableCollateral =
                INetworkMiddleware(network).slashableCollateral(_agent, _slashTimestamp);
            if (networkSlashableCollateral == 0) continue;

            // Calculate this network's share of the total amount to slash
            uint256 networkSlash = _amount * 1e18 / networkSlashableCollateral;
            INetworkMiddleware(network).slash(_agent, _liquidator, networkSlash, _slashTimestamp);
            emit SlashNetwork(network, networkSlash);
        }
    }

    /// @notice Distribute rewards to networks covering an agent proportionally to their coverage
    /// @param _agent The agent address
    /// @param _asset The reward token address
    function distributeRewards(address _agent, address _asset) external {
        DelegationStorage storage $ = getDelegationStorage();
        uint256 _amount = IERC20(_asset).balanceOf(address(this));

        uint256 totalCoverage = coverage(_agent);
        // here we cannot revert because the agent might not have any coverage
        // in case we are liquidating the current agent due to 0 coverage
        if (totalCoverage == 0) return;

        // Distribute to each network based on their coverage proportion
        for (uint i; i < $.networks[_agent].length; ++i) {
            address network = $.networks[_agent][i];
            uint256 networkCoverage = coverageByNetwork(_agent, network);
            if (networkCoverage == 0) continue;

            uint256 networkReward = _amount * networkCoverage / totalCoverage;
            IERC20(_asset).safeTransfer(network, networkReward);
            INetworkMiddleware(network).distributeRewards(_agent, _asset);
            emit NetworkReward(network, _asset, networkReward);
        }

        emit DistributeReward(_agent, _asset, _amount);
    }

    /// @notice Set the last borrow timestamp for an agent
    /// @param _agent Agent address
    function setLastBorrow(address _agent) external checkAccess(this.setLastBorrow.selector) {
        DelegationStorage storage $ = getDelegationStorage();
        $.agentData[_agent].lastBorrow = block.timestamp;
    }

    /// @notice Add agent to be delegated to
    /// @param _agent Agent address
    /// @param _ltv Loan to value ratio
    /// @param _liquidationThreshold Liquidation threshold
    function addAgent(address _agent, uint256 _ltv, uint256 _liquidationThreshold)
        external
        checkAccess(this.addAgent.selector)
    {
        // if liquidation threshold or ltv is greater than 100%, agent
        // could borrow more than they are collateralized for
        if (_liquidationThreshold > 1e27) revert InvalidLiquidationThreshold();
        if (_ltv > 1e27) revert InvalidLtv();

        DelegationStorage storage $ = getDelegationStorage();

        // If the agent already exists, we revert
        if ($.agentData[_agent].exists) revert DuplicateAgent();

        $.agents.push(_agent);
        $.agentData[_agent].ltv = _ltv;
        $.agentData[_agent].liquidationThreshold = _liquidationThreshold;
        $.agentData[_agent].exists = true;
        emit AddAgent(_agent, _ltv, _liquidationThreshold);
    }

    /// @notice Modify an agents config only callable by the operator
    /// @param _agent the agent to modify
    /// @param _ltv Loan to value ratio
    /// @param _liquidationThreshold Liquidation threshold
    function modifyAgent(address _agent, uint256 _ltv, uint256 _liquidationThreshold)
        external
        checkAccess(this.modifyAgent.selector)
    {
        // if liquidation threshold or ltv is greater than 100%, agent
        // could borrow more than they are collateralized for
        if (_liquidationThreshold > 1e27) revert InvalidLiquidationThreshold();
        if (_ltv > 1e27) revert InvalidLtv();

        DelegationStorage storage $ = getDelegationStorage();

        // Check that the agent exists
        if (!$.agentData[_agent].exists) revert AgentDoesNotExist();

        $.agentData[_agent].ltv = _ltv;
        $.agentData[_agent].liquidationThreshold = _liquidationThreshold;
        emit ModifyAgent(_agent, _ltv, _liquidationThreshold);
    }

    /// @notice Register a new network
    /// @param _agent Agent address
    /// @param _network Network address
    function registerNetwork(address _agent, address _network) external checkAccess(this.registerNetwork.selector) {
        DelegationStorage storage $ = getDelegationStorage();

        // Check for duplicates
        if ($.networkExistsForAgent[_agent][_network]) revert DuplicateNetwork();

        $.networks[_agent].push(_network);
        $.networkExistsForAgent[_agent][_network] = true;
        emit RegisterNetwork(_agent, _network);
    }

    /// @dev Only admin can upgrade
    function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { Access } from "../../../access/Access.sol";
import { IOracle } from "../../../interfaces/IOracle.sol";
import { IStakerRewards } from "../../../interfaces/IStakerRewards.sol";

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { IBurnerRouter } from "@symbioticfi/burners/src/interfaces/router/IBurnerRouter.sol";

import { IMiddleware } from "../../../interfaces/IMiddleware.sol";

import { INetworkMiddleware } from "../../../interfaces/INetworkMiddleware.sol";
import { NetworkMiddlewareStorageUtils } from "../../../storage/NetworkMiddlewareStorageUtils.sol";
import { Subnetwork } from "@symbioticfi/core/src/contracts/libraries/Subnetwork.sol";
import { IEntity } from "@symbioticfi/core/src/interfaces/common/IEntity.sol";
import { IRegistry } from "@symbioticfi/core/src/interfaces/common/IRegistry.sol";
import { IBaseDelegator } from "@symbioticfi/core/src/interfaces/delegator/IBaseDelegator.sol";
import { ISlasher } from "@symbioticfi/core/src/interfaces/slasher/ISlasher.sol";
import { IVault } from "@symbioticfi/core/src/interfaces/vault/IVault.sol";

/// @title Cap Symbiotic Network Middleware Contract
/// @author Cap Labs
/// @notice This contract manages the symbiotic collateral and slashing.
contract NetworkMiddleware is INetworkMiddleware, UUPSUpgradeable, Access, NetworkMiddlewareStorageUtils {
    using SafeERC20 for IERC20;

    /// @notice Initialize
    /// @param _accessControl Access control address
    /// @param _network Network address
    /// @param _vaultRegistry Vault registry address
    /// @param _oracle Oracle address
    /// @param _requiredEpochDuration Required epoch duration in seconds
    /// @param _feeAllowed Fee allowed to be charged on rewards by restakers
    function initialize(
        address _accessControl,
        address _network,
        address _vaultRegistry,
        address _oracle,
        uint48 _requiredEpochDuration,
        uint256 _feeAllowed
    ) external initializer {
        __Access_init(_accessControl);
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();
        $.network = _network;
        $.vaultRegistry = _vaultRegistry;
        $.oracle = _oracle;
        $.requiredEpochDuration = _requiredEpochDuration;
        $.feeAllowed = _feeAllowed;
    }

    /// @notice Register vault to be used as collateral within the CAP system
    /// @param _vault Vault address
    /// @param _agents Agents supported by the vault
    function registerVault(address _vault, address _stakerRewarder, address[] calldata _agents)
        external
        checkAccess(this.registerVault.selector)
    {
        _verifyVault(_vault);
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();
        $.stakerRewarders[_vault] = _stakerRewarder;
        for (uint256 i; i < _agents.length; ++i) {
            $.vaults[_agents[i]].push(_vault);
        }
        emit VaultRegistered(_vault);
    }

    /// @notice Set fee allowed
    /// @param _feeAllowed Fee allowed to be charged on rewards by restakers
    function setFeeAllowed(uint256 _feeAllowed) external checkAccess(this.setFeeAllowed.selector) {
        getNetworkMiddlewareStorage().feeAllowed = _feeAllowed;
    }

    /// @notice Slash delegation and send to recipient
    /// @param _agent Agent address
    /// @param _recipient Recipient of the slashed assets
    /// @param _slashShare Percentage of delegation to slash encoded with 18 decimals
    /// @param _timestamp Timestamp to slash at
    function slash(address _agent, address _recipient, uint256 _slashShare, uint48 _timestamp)
        external
        checkAccess(this.slash.selector)
    {
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();

        for (uint256 i; i < $.vaults[_agent].length; ++i) {
            IVault vault = IVault($.vaults[_agent][i]);

            (, uint256 totalSlashableCollateral) =
                slashableCollateralByVault($.network, _agent, address(vault), $.oracle, _timestamp);
            if (totalSlashableCollateral == 0) continue;

            // Round up in favor of the liquidator
            uint256 slashShareOfCollateral = (totalSlashableCollateral * _slashShare / 1e18) + 1;

            // If the slash share is greater than the total slashable collateral, set it to the total slashable collateral
            if (slashShareOfCollateral > totalSlashableCollateral) {
                slashShareOfCollateral = totalSlashableCollateral;
            }

            ISlasher(vault.slasher()).slash(
                subnetwork(_agent), _agent, slashShareOfCollateral, _timestamp, new bytes(0)
            );

            // TODO: the burner could be a non routing burner, could add hooks?
            IBurnerRouter(vault.burner()).triggerTransfer(address(this));
            IERC20(vault.collateral()).safeTransfer(_recipient, slashShareOfCollateral);

            emit Slash(_agent, _recipient, slashShareOfCollateral);
        }
    }

    /// @dev Get vault info
    /// @param _network Network address
    /// @param _agent Agent address
    /// @param _vault Vault address
    /// @param _oracle Oracle address
    /// @return burnerRouter The burner router contract
    /// @return decimals The collateral token decimals
    /// @return collateralPrice The collateral token price
    function _getVaultInfo(address _network, address _agent, address _vault, address _oracle)
        private
        view
        returns (IBurnerRouter burnerRouter, uint8 decimals, uint256 collateralPrice)
    {
        burnerRouter = IBurnerRouter(IVault(_vault).burner());

        // Check pending receivers
        (address pendingReceiver,) = burnerRouter.pendingNetworkReceiver(_network);
        if (pendingReceiver != address(0) && pendingReceiver != address(this)) {
            return (IBurnerRouter(address(0)), 0, 0);
        }

        (pendingReceiver,) = burnerRouter.pendingOperatorNetworkReceiver(_network, _agent);
        if (pendingReceiver != address(0) && pendingReceiver != address(this)) {
            return (IBurnerRouter(address(0)), 0, 0);
        }

        address collateralAddress = IVault(_vault).collateral();
        decimals = IERC20Metadata(collateralAddress).decimals();
        (collateralPrice,) = IOracle(_oracle).getPrice(collateralAddress);
    }

    /// @notice Coverage of an agent by a specific vault at a given timestamp
    /// @param _network Network address
    /// @param _agent Agent address
    /// @param _vault Vault address
    /// @param _oracle Oracle address
    /// @param _timestamp Timestamp to check coverage at
    /// @return collateralValue Coverage value in USD (8 decimals)
    /// @return collateral Coverage amount in the vault's collateral token decimals
    function coverageByVault(address _network, address _agent, address _vault, address _oracle, uint48 _timestamp)
        public
        view
        returns (uint256 collateralValue, uint256 collateral)
    {
        (IBurnerRouter burnerRouter, uint8 decimals, uint256 collateralPrice) =
            _getVaultInfo(_network, _agent, _vault, _oracle);

        if (address(burnerRouter) == address(0)) return (0, 0);

        collateral = IBaseDelegator(IVault(_vault).delegator()).stakeAt(subnetwork(_agent), _agent, _timestamp, "");
        collateralValue = collateral * collateralPrice / (10 ** decimals);
    }

    /// @notice Slashable collateral of an agent by a specific vault at a given timestamp
    /// @param _network Network address
    /// @param _agent Agent address
    /// @param _vault Vault address
    /// @param _oracle Oracle address
    /// @param _timestamp Timestamp to check slashable collateral at
    /// @return collateralValue Slashable collateral value in USD (8 decimals)
    function slashableCollateralByVault(
        address _network,
        address _agent,
        address _vault,
        address _oracle,
        uint48 _timestamp
    ) public view returns (uint256 collateralValue, uint256 collateral) {
        (IBurnerRouter burnerRouter, uint8 decimals, uint256 collateralPrice) =
            _getVaultInfo(_network, _agent, _vault, _oracle);

        if (address(burnerRouter) == address(0)) return (0, 0);

        ISlasher slasher = ISlasher(IVault(_vault).slasher());
        collateral = slasher.slashableStake(subnetwork(_agent), _agent, _timestamp, "");
        collateralValue = collateral * collateralPrice / (10 ** decimals);
    }

    /// @notice Coverage of an agent by Symbiotic vaults
    /// @param _agent Agent address
    /// @return delegation Delegation amount in USD (8 decimals)
    function coverage(address _agent) public view returns (uint256 delegation) {
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();
        address[] memory _vaults = $.vaults[_agent];
        address _network = $.network;
        address _oracle = $.oracle;
        uint48 _timestamp = uint48(block.timestamp);

        for (uint256 i = 0; i < _vaults.length; i++) {
            (uint256 value,) = coverageByVault(_network, _agent, _vaults[i], _oracle, _timestamp);
            delegation += value;
        }
    }

    /// @notice Slashable collateral of an agent by Symbiotic vaults
    /// @param _agent Agent address
    /// @param _timestamp Timestamp to check slashable collateral at
    /// @return _slashableCollateral Slashable collateral amount in USD (8 decimals)
    function slashableCollateral(address _agent, uint48 _timestamp)
        external
        view
        returns (uint256 _slashableCollateral)
    {
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();
        address[] memory _vaults = $.vaults[_agent];
        address _network = $.network;
        address _oracle = $.oracle;

        for (uint256 i = 0; i < _vaults.length; i++) {
            (uint256 value,) = slashableCollateralByVault(_network, _agent, _vaults[i], _oracle, _timestamp);
            _slashableCollateral += value;
        }
    }

    /// @notice Subnetwork id
    /// @dev Creates a collision resistant uint96 identifier by taking keccak256 hash of agent address
    /// and using the first 96 bits of the hash
    /// @param _agent Agent address
    /// @return id Subnetwork identifier (first 96 bits of keccak256 hash of agent address)
    function subnetworkIdentifier(address _agent) public pure returns (uint96 id) {
        bytes32 hash = keccak256(abi.encodePacked(_agent));
        id = uint96(uint256(hash)); // Takes first 96 bits of hash
    }

    /// @notice Subnetwork id concatenated with network address
    /// @return id Subnetwork id
    function subnetwork(address _agent) public view returns (bytes32 id) {
        id = Subnetwork.subnetwork(getNetworkMiddlewareStorage().network, subnetworkIdentifier(_agent));
    }

    /// @notice Registered vaults for an agent
    /// @param _agent Agent address
    /// @return vaultAddresses Vault addresses
    function vaults(address _agent) external view returns (address[] memory vaultAddresses) {
        vaultAddresses = getNetworkMiddlewareStorage().vaults[_agent];
    }

    /// @dev Verify a vault has the required specifications
    /// @param _vault Vault address
    function _verifyVault(address _vault) internal view {
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();

        if (!IRegistry($.vaultRegistry).isEntity(_vault)) {
            revert NotVault();
        }

        if (!IVault(_vault).isInitialized()) revert VaultNotInitialized();

        uint48 vaultEpoch = IVault(_vault).epochDuration();
        if (vaultEpoch < $.requiredEpochDuration) revert InvalidEpochDuration($.requiredEpochDuration, vaultEpoch);

        address slasher = IVault(_vault).slasher();
        uint64 slasherType = IEntity(slasher).TYPE();
        if (slasher == address(0)) revert NoSlasher();
        if (slasherType != uint64(INetworkMiddleware.SlasherType.INSTANT)) revert InvalidSlasher();

        address burner = IVault(_vault).burner();
        if (burner == address(0)) revert NoBurner();
        address receiver = IBurnerRouter(burner).networkReceiver($.network);
        if (receiver != address(this)) revert InvalidBurnerRouter();

        address delegator = IVault(_vault).delegator();
        uint64 delegatorType = IEntity(delegator).TYPE();
        if (delegatorType != uint64(INetworkMiddleware.DelegatorType.NETWORK_RESTAKE)) revert InvalidDelegator();
    }

    /// @notice Distribute rewards accumulated by the agent borrowing
    /// @param _agent Agent address
    /// @param _token Token address
    function distributeRewards(address _agent, address _token) external checkAccess(this.distributeRewards.selector) {
        NetworkMiddlewareStorage storage $ = getNetworkMiddlewareStorage();
        uint256 totalCollateralValue = coverage(_agent);
        uint256 _amount = IERC20(_token).balanceOf(address(this));

        // here, distribute proportionally to the collateral value of the vaults
        address[] memory _vaults = $.vaults[_agent];
        for (uint256 i = 0; i < _vaults.length; i++) {
            address vault = _vaults[i];
            (uint256 collateralValue,) = coverageByVault($.network, _agent, vault, $.oracle, uint48(block.timestamp));
            uint256 reward = _amount * collateralValue / totalCollateralValue;
            address stakerRewarder = $.stakerRewarders[vault];
            if (stakerRewarder == address(0)) revert NoStakerRewarder();

            IERC20(_token).forceApprove(address(IStakerRewards(stakerRewarder)), reward);
            IStakerRewards(stakerRewarder).distributeRewards(
                $.network, _token, reward, abi.encode(uint48(block.timestamp - 1), $.feeAllowed, "", "")
            );
        }
    }

    /// @dev Only admin can upgrade
    function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import { Access } from "../access/Access.sol";

import { ILender } from "../interfaces/ILender.sol";
import { LenderStorageUtils } from "../storage/LenderStorageUtils.sol";
import { BorrowLogic } from "./libraries/BorrowLogic.sol";
import { LiquidationLogic } from "./libraries/LiquidationLogic.sol";
import { ReserveLogic } from "./libraries/ReserveLogic.sol";
import { ViewLogic } from "./libraries/ViewLogic.sol";

/// @title Lender for covered agents
/// @author kexley, @capLabs
/// @notice Whitelisted tokens are borrowed and repaid from this contract by covered agents.
/// @dev Borrow interest rates are calculated from the underlying utilization rates of the assets
/// in the vaults.
contract Lender is ILender, UUPSUpgradeable, Access, LenderStorageUtils {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /// @notice Initialize the lender
    /// @param _accessControl Access control address
    /// @param _delegation Delegation address
    /// @param _oracle Oracle address
    /// @param _targetHealth Target health after liquidations (scaled by 1e27)
    /// @param _grace Grace period in seconds before an agent becomes liquidatable
    /// @param _expiry Expiry period in seconds after which an agent cannot be liquidated until called again
    /// @param _bonusCap Bonus cap for liquidations (scaled by 1e27)
    /// @param _emergencyLiquidationThreshold Liquidation threshold below which grace periods are voided (scaled by 1e27)
    function initialize(
        address _accessControl,
        address _delegation,
        address _oracle,
        uint256 _targetHealth,
        uint256 _grace,
        uint256 _expiry,
        uint256 _bonusCap,
        uint256 _emergencyLiquidationThreshold
    ) external initializer {
        __Access_init(_accessControl);
        __UUPSUpgradeable_init();

        if (_delegation == address(0) || _oracle == address(0)) revert ZeroAddressNotValid();

        // TODO: remove this
        LenderStorage storage $ = getLenderStorage();
        $.delegation = _delegation;
        $.oracle = _oracle;
        $.targetHealth = _targetHealth;
        $.grace = _grace;
        $.expiry = _expiry;
        $.bonusCap = _bonusCap;
        $.emergencyLiquidationThreshold = _emergencyLiquidationThreshold;
    }

    /// @notice Borrow an asset
    /// @param _asset Asset to borrow
    /// @param _amount Amount to borrow
    /// @param _receiver Receiver of the borrowed asset
    function borrow(address _asset, uint256 _amount, address _receiver) external {
        BorrowLogic.borrow(
            getLenderStorage(), BorrowParams({ agent: msg.sender, asset: _asset, amount: _amount, receiver: _receiver })
        );
    }

    /// @notice Repay an asset
    /// @param _asset Asset to repay
    /// @param _amount Amount to repay
    /// @param _agent Repay on behalf of another borrower
    /// @return repaid Actual amount repaid
    function repay(address _asset, uint256 _amount, address _agent) external returns (uint256 repaid) {
        if (_agent == address(0) || _asset == address(0)) revert ZeroAddressNotValid();
        repaid = BorrowLogic.repay(
            getLenderStorage(), RepayParams({ agent: _agent, asset: _asset, amount: _amount, caller: msg.sender })
        );
    }

    /// @notice Realize interest for an asset
    /// @param _asset Asset to realize interest for
    /// @param _amount Amount of interest to realize (type(uint).max for all available interest)
    /// @return actualRealized Actual amount realized
    function realizeInterest(address _asset, uint256 _amount) external returns (uint256 actualRealized) {
        actualRealized =
            BorrowLogic.realizeInterest(getLenderStorage(), RealizeInterestParams({ asset: _asset, amount: _amount }));
    }

    /// @notice Calculate the maximum interest that can be realized
    /// @param _asset Asset to calculate max realization for
    /// @return _maxRealization Maximum interest that can be realized
    function maxRealization(address _asset) external view returns (uint256 _maxRealization) {
        _maxRealization = BorrowLogic.maxRealization(getLenderStorage(), _asset);
    }

    /// @notice Initiate liquidation of an agent when the health is below 1
    /// @param _agent Agent address
    function initiateLiquidation(address _agent) external {
        LiquidationLogic.initiateLiquidation(getLenderStorage(), _agent);
    }

    /// @notice Cancel liquidation of an agent when the health is above 1
    /// @param _agent Agent address
    function cancelLiquidation(address _agent) external {
        LiquidationLogic.cancelLiquidation(getLenderStorage(), _agent);
    }

    /// @notice Liquidate an agent when the health is below 1
    /// @param _agent Agent address
    /// @param _asset Asset to repay
    /// @param _amount Amount of asset to repay on behalf of the agent
    /// @param liquidatedValue Value of the liquidation returned to the liquidator
    function liquidate(address _agent, address _asset, uint256 _amount) external returns (uint256 liquidatedValue) {
        if (_agent == address(0) || _asset == address(0)) revert ZeroAddressNotValid();
        liquidatedValue = LiquidationLogic.liquidate(
            getLenderStorage(), RepayParams({ agent: _agent, asset: _asset, amount: _amount, caller: msg.sender })
        );
    }

    /// @notice Calculate the agent data
    /// @param _agent Address of agent
    /// @return totalDelegation Total delegation of an agent in USD, encoded with 8 decimals
    /// @return totalDebt Total debt of an agent in USD, encoded with 8 decimals
    /// @return ltv Loan to value ratio, encoded in ray (1e27)
    /// @return liquidationThreshold Liquidation ratio of an agent, encoded in ray (1e27)
    /// @return health Health status of an agent, encoded in ray (1e27)
    function agent(address _agent)
        external
        view
        returns (uint256 totalDelegation, uint256 totalDebt, uint256 ltv, uint256 liquidationThreshold, uint256 health)
    {
        (totalDelegation, totalDebt, ltv, liquidationThreshold, health) = ViewLogic.agent(getLenderStorage(), _agent);
    }

    /// @notice Calculate the maximum amount that can be borrowed for a given asset
    /// @param _agent Agent address
    /// @param _asset Asset to borrow
    /// @return maxBorrowableAmount Maximum amount that can be borrowed in asset decimals
    function maxBorrowable(address _agent, address _asset) external view returns (uint256 maxBorrowableAmount) {
        if (_agent == address(0) || _asset == address(0)) revert ZeroAddressNotValid();
        maxBorrowableAmount = ViewLogic.maxBorrowable(getLenderStorage(), _agent, _asset);
    }

    /// @notice Calculate the maximum amount that can be liquidated for a given asset
    /// @param _agent Agent address
    /// @param _asset Asset to liquidate
    /// @return maxLiquidatableAmount Maximum amount that can be liquidated in asset decimals
    function maxLiquidatable(address _agent, address _asset) external view returns (uint256 maxLiquidatableAmount) {
        if (_agent == address(0) || _asset == address(0)) revert ZeroAddressNotValid();
        maxLiquidatableAmount = ViewLogic.maxLiquidatable(getLenderStorage(), _agent, _asset);
    }

    /// @notice Get the current debt balances for an agent for a specific asset
    /// @param _agent Agent address to check debt for
    /// @param _asset Asset to check debt for
    /// @return principalDebt Principal debt amount in asset decimals
    /// @return interestDebt Interest debt amount in asset decimals
    /// @return restakerDebt Restaker debt amount in asset decimals
    function debt(address _agent, address _asset)
        external
        view
        returns (uint256 principalDebt, uint256 interestDebt, uint256 restakerDebt)
    {
        if (_agent == address(0) || _asset == address(0)) revert ZeroAddressNotValid();
        (principalDebt, interestDebt, restakerDebt) = ViewLogic.debt(getLenderStorage(), _agent, _asset);
    }

    /// @notice Add an asset to the Lender
    /// @param _params Parameters to add an asset
    function addAsset(AddAssetParams calldata _params) external checkAccess(this.addAsset.selector) {
        LenderStorage storage $ = getLenderStorage();
        if (!ReserveLogic.addAsset($, _params)) ++$.reservesCount;
    }

    /// @notice Remove asset from lending when there is no borrows
    /// @param _asset Asset address
    function removeAsset(address _asset) external checkAccess(this.removeAsset.selector) {
        if (_asset == address(0)) revert ZeroAddressNotValid();
        ReserveLogic.removeAsset(getLenderStorage(), _asset);
    }

    /// @notice Pause an asset from being borrowed
    /// @param _asset Asset address
    /// @param _pause True if pausing or false if unpausing
    function pauseAsset(address _asset, bool _pause) external checkAccess(this.pauseAsset.selector) {
        if (_asset == address(0)) revert ZeroAddressNotValid();
        ReserveLogic.pauseAsset(getLenderStorage(), _asset, _pause);
    }

    /// @notice The total number of reserves
    /// @return count Number of reserves
    function reservesCount() external view returns (uint256 count) {
        count = getLenderStorage().reservesCount;
    }

    /// @notice The grace period duration
    /// @return gracePeriod Grace period in seconds
    function grace() external view returns (uint256 gracePeriod) {
        gracePeriod = getLenderStorage().grace;
    }

    /// @notice The expiry period duration
    /// @return expiryPeriod Expiry period in seconds
    function expiry() external view returns (uint256 expiryPeriod) {
        expiryPeriod = getLenderStorage().expiry;
    }

    /// @notice The target health factor
    /// @return target Target health factor scaled to 1e27
    function targetHealth() external view returns (uint256 target) {
        target = getLenderStorage().targetHealth;
    }

    /// @notice The liquidation bonus cap
    /// @return cap Bonus cap scaled to 1e27
    function bonusCap() external view returns (uint256 cap) {
        cap = getLenderStorage().bonusCap;
    }

    /// @notice The emergency liquidation threshold
    /// @return threshold Threshold scaled to 1e27
    function emergencyLiquidationThreshold() external view returns (uint256 threshold) {
        threshold = getLenderStorage().emergencyLiquidationThreshold;
    }

    /// @notice The liquidation start time for an agent
    /// @param _agent Address of the agent
    /// @return startTime Timestamp when liquidation was initiated
    function liquidationStart(address _agent) external view returns (uint256 startTime) {
        startTime = getLenderStorage().liquidationStart[_agent];
    }

    /// @notice The reserve data for an asset
    /// @param _asset Address of the asset
    /// @return id Id of the reserve
    /// @return vault Address of the vault
    /// @return principalDebtToken Address of the principal debt token
    /// @return restakerDebtToken Address of the restaker debt token
    /// @return interestDebtToken Address of the interest debt token
    /// @return interestReceiver Address of the interest receiver
    /// @return decimals Decimals of the asset
    /// @return paused True if the asset is paused, false otherwise
    /// @return realizedInterest Realized interest of the asset
    function reservesData(address _asset)
        external
        view
        returns (
            uint256 id,
            address vault,
            address principalDebtToken,
            address restakerDebtToken,
            address interestDebtToken,
            address interestReceiver,
            uint8 decimals,
            bool paused,
            uint256 realizedInterest
        )
    {
        ReserveData storage reserve = getLenderStorage().reservesData[_asset];
        id = reserve.id;
        vault = reserve.vault;
        principalDebtToken = reserve.principalDebtToken;
        restakerDebtToken = reserve.restakerDebtToken;
        interestDebtToken = reserve.interestDebtToken;
        interestReceiver = reserve.interestReceiver;
        decimals = reserve.decimals;
        paused = reserve.paused;
        realizedInterest = reserve.realizedInterest;
    }

    function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}


Vulnerability type: CWE-435|CWE-436|CWE-437