// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PluginOracleAdapter} from "./PluginOracleAdapter.sol"; import {Owned} from "./utils/Owned.sol"; interface IERC4626Rate { function convertToAssets(uint256 shares) external view returns (uint256); function decimals() external view returns (uint8); } /// @title ERC4626RateGuard /// @notice Guarded share price = assetPrice * (assets per 1 share), in the asset oracle's decimals. /// The live vault rate must sit within [minRate, maxRate] and within `maxDeviationBps` /// of the reference rate. Only the keeper or owner may move the reference rate, and each /// update moves it at most `maxStepBps` toward the live rate. Nothing is permissionless. contract ERC4626RateGuard is Owned { IERC4626Rate public immutable vault; PluginOracleAdapter public immutable assetOracle; uint256 public immutable oneShare; uint256 public immutable assetUnit; // 10**assetDecimals uint256 public immutable minRate; uint256 public immutable maxRate; uint256 public immutable maxDeviationBps; uint256 public immutable maxStepBps; address public keeper; uint256 public refRate; uint256 public lastUpdate; error RateOutOfBounds(uint256 rate); error RateDeviation(uint256 rate, uint256 ref); error NotKeeper(address caller); error BadConfig(); event KeeperSet(address keeper); event RefRateUpdated(uint256 oldRate, uint256 newRate, uint256 liveRate); constructor( IERC4626Rate _vault, PluginOracleAdapter _assetOracle, uint8 assetDecimals, uint256 _minRate, uint256 _maxRate, uint256 _maxDeviationBps, uint256 _maxStepBps, address _owner ) Owned(_owner) { if ( _minRate == 0 || _minRate > _maxRate || _maxDeviationBps == 0 || _maxDeviationBps > 10_000 || _maxStepBps == 0 || _maxStepBps > 10_000 ) revert BadConfig(); vault = _vault; assetOracle = _assetOracle; oneShare = 10 ** _vault.decimals(); assetUnit = 10 ** assetDecimals; minRate = _minRate; maxRate = _maxRate; maxDeviationBps = _maxDeviationBps; maxStepBps = _maxStepBps; uint256 r = _vault.convertToAssets(oneShare); if (r < _minRate || r > _maxRate) revert RateOutOfBounds(r); refRate = r; lastUpdate = block.timestamp; } function setKeeper(address k) external onlyOwner { keeper = k; emit KeeperSet(k); } function liveRate() public view returns (uint256 r) { r = vault.convertToAssets(oneShare); if (r < minRate || r > maxRate) revert RateOutOfBounds(r); } /// @notice Live rate, checked against bounds and the reference rate. function guardedRate() public view returns (uint256 r) { r = liveRate(); uint256 diff = r > refRate ? r - refRate : refRate - r; if (diff * 10_000 > refRate * maxDeviationBps) revert RateDeviation(r, refRate); } function guardedPrice() external view returns (uint256) { return assetOracle.latestPrice() * guardedRate() / assetUnit; } /// @notice Keeper/owner only: move the reference rate toward the live rate, capped per update. function update() external returns (uint256 newRef) { if (msg.sender != keeper && msg.sender != owner) revert NotKeeper(msg.sender); uint256 r = liveRate(); uint256 old = refRate; uint256 step = old * maxStepBps / 10_000; if (r > old) newRef = r - old > step ? old + step : r; else newRef = old - r > step ? old - step : r; refRate = newRef; lastUpdate = block.timestamp; emit RefRateUpdated(old, newRef, r); } }