ParameterRegistry

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/ParameterRegistry.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.26;

import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";

interface AggregatorV3Interface {
    function latestRoundData()
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

    function getRoundData(uint80 _roundId)
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

interface OracleProxyInterface {
    function aggregator() external view returns (address);
}

/**
 * @title ParameterRegistry
 * @dev Multi-asset parameter registry for offchain oracle network consumption
 */
contract ParameterRegistry is Ownable2Step {
    uint64 public constant MAX_EXPECTED_APY_LIMIT = 20_000; // Max 200% (20000 BPS)
    uint32 public constant MAX_UPPER_BOUND_TOLERANCE = 250; // Max 2.5% (250 BPS)
    uint32 public constant MAX_LOWER_BOUND_TOLERANCE = 250; // Max 2.5% (250 BPS)
    uint32 public constant MAX_DISCOUNT_LIMIT = 250; // Max 2.5% (250 BPS)

    struct AssetConfig {
        string name;
        address oracle; // 20 bytes
        bool exists; // 1 byte
        uint80 lookbackWindowSize; // 10 bytes (up to 1.2M blocks)
        uint64 maxExpectedApy; // 8 bytes (BPS format, max ~1.84 * 10^15%)
        uint32 lowerBoundTolerance; // 4 bytes (BPS format, max ~42949672 = 429496.72%)
        uint32 upperBoundTolerance; // 4 bytes (BPS format, max ~42949672 = 429496.72%)
        uint32 maxDiscount; // 4 bytes (BPS format, max ~42949672 = 429496.72%)
        bool isUpperBoundEnabled; // 1 byte
        bool isLowerBoundEnabled; // 1 byte
        bool isActionTakingEnabled; // 1 byte
    }

    address public updater;
    mapping(address _assetAddress => AssetConfig _assetConfig) private assetConfigs;

    event UpdaterChanged(address indexed previousUpdater, address indexed newUpdater);
    event AssetParametersSet(
        address indexed asset,
        uint64 maxExpectedApy,
        uint32 upperBoundTolerance,
        uint32 lowerBoundTolerance,
        uint32 maxDiscount,
        uint80 lookbackWindowSize,
        bool isUpperBoundEnabled,
        bool isLowerBoundEnabled,
        bool isActionTakingEnabled
    );
    event AssetDeleted(address indexed asset);
    event AssetNameSet(address indexed asset, string name);
    event AssetOracleSet(address indexed asset, address oracle);
    event LookbackWindowSizeSet(address indexed asset, uint80 lookbackWindowSize);
    event MaxExpectedApySet(address indexed asset, uint64 maxExpectedApy);
    event UpperBoundToleranceSet(address indexed asset, uint32 upperBoundTolerance);
    event LowerBoundToleranceSet(address indexed asset, uint32 lowerBoundTolerance);
    event MaxDiscountSet(address indexed asset, uint32 maxDiscount);
    event IsUpperBoundEnabledSet(address indexed asset, bool isUpperBoundEnabled);
    event IsLowerBoundEnabledSet(address indexed asset, bool isLowerBoundEnabled);
    event IsActionTakingEnabledSet(address indexed asset, bool isActionTakingEnabled);

    error OnlyUpdater();
    error AssetNotFound();
    error ZeroAddress();
    error OracleNotSet();
    error InvalidLookbackWindow();
    error MaxExpectedApyTooHigh(uint64 value);
    error UpperBoundToleranceTooHigh(uint32 value);
    error LowerBoundToleranceTooHigh(uint32 value);
    error MaxDiscountTooHigh(uint32 value);

    modifier onlyUpdater() {
        if (msg.sender != updater) revert OnlyUpdater();
        _;
    }

    constructor(address _owner, address _updater) Ownable(_owner) {
        if (_updater == address(0)) revert ZeroAddress();
        emit UpdaterChanged(address(0), updater = _updater);
    }

    function setUpdater(address _updater) external onlyOwner {
        if (_updater == address(0)) revert ZeroAddress();
        address previousUpdater = updater;
        emit UpdaterChanged(previousUpdater, updater = _updater);
    }

    function setParametersForAsset(
        address asset,
        string calldata assetName,
        address oracle,
        uint64 maxExpectedApy,
        uint32 upperBoundTolerance,
        uint32 lowerBoundTolerance,
        uint32 maxDiscount,
        uint80 lookbackWindowSize,
        bool isUpperBoundEnabled,
        bool isLowerBoundEnabled,
        bool isActionTakingEnabled
    )
        external
        onlyUpdater
    {
        if (asset == address(0)) revert ZeroAddress();
        if (oracle == address(0)) revert ZeroAddress();
        if (maxExpectedApy > MAX_EXPECTED_APY_LIMIT) revert MaxExpectedApyTooHigh(maxExpectedApy);
        if (upperBoundTolerance > MAX_UPPER_BOUND_TOLERANCE) revert UpperBoundToleranceTooHigh(upperBoundTolerance);
        if (lowerBoundTolerance > MAX_LOWER_BOUND_TOLERANCE) revert LowerBoundToleranceTooHigh(lowerBoundTolerance);
        if (maxDiscount > MAX_DISCOUNT_LIMIT) revert MaxDiscountTooHigh(maxDiscount);
        if (lookbackWindowSize == 0) revert InvalidLookbackWindow();

        assetConfigs[asset] = AssetConfig({
            name: assetName,
            oracle: oracle,
            exists: true,
            lookbackWindowSize: lookbackWindowSize,
            maxExpectedApy: maxExpectedApy,
            lowerBoundTolerance: lowerBoundTolerance,
            upperBoundTolerance: upperBoundTolerance,
            maxDiscount: maxDiscount,
            isUpperBoundEnabled: isUpperBoundEnabled,
            isLowerBoundEnabled: isLowerBoundEnabled,
            isActionTakingEnabled: isActionTakingEnabled
        });

        emit AssetNameSet(asset, assetName);
        emit AssetOracleSet(asset, oracle);
        emit AssetParametersSet(
            asset,
            maxExpectedApy,
            upperBoundTolerance,
            lowerBoundTolerance,
            maxDiscount,
            lookbackWindowSize,
            isUpperBoundEnabled,
            isLowerBoundEnabled,
            isActionTakingEnabled
        );
    }

    function setMaxExpectedApy(address asset, uint64 _maxExpectedApy) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (_maxExpectedApy > MAX_EXPECTED_APY_LIMIT) revert MaxExpectedApyTooHigh(_maxExpectedApy);
        assetConfigs[asset].maxExpectedApy = _maxExpectedApy;
        emit MaxExpectedApySet(asset, _maxExpectedApy);
    }

    function setUpperBoundTolerance(address asset, uint32 _upperBoundTolerance) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (_upperBoundTolerance > MAX_UPPER_BOUND_TOLERANCE) revert UpperBoundToleranceTooHigh(_upperBoundTolerance);
        assetConfigs[asset].upperBoundTolerance = _upperBoundTolerance;
        emit UpperBoundToleranceSet(asset, _upperBoundTolerance);
    }

    function setLowerBoundTolerance(address asset, uint32 _lowerBoundTolerance) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (_lowerBoundTolerance > MAX_LOWER_BOUND_TOLERANCE) revert LowerBoundToleranceTooHigh(_lowerBoundTolerance);
        assetConfigs[asset].lowerBoundTolerance = _lowerBoundTolerance;
        emit LowerBoundToleranceSet(asset, _lowerBoundTolerance);
    }

    function setMaxDiscount(address asset, uint32 _maxDiscount) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (_maxDiscount > MAX_DISCOUNT_LIMIT) revert MaxDiscountTooHigh(_maxDiscount);
        assetConfigs[asset].maxDiscount = _maxDiscount;
        emit MaxDiscountSet(asset, _maxDiscount);
    }

    function setIsUpperBoundEnabled(address asset, bool _isUpperBoundEnabled) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        assetConfigs[asset].isUpperBoundEnabled = _isUpperBoundEnabled;
        emit IsUpperBoundEnabledSet(asset, _isUpperBoundEnabled);
    }

    function setIsLowerBoundEnabled(address asset, bool _isLowerBoundEnabled) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        assetConfigs[asset].isLowerBoundEnabled = _isLowerBoundEnabled;
        emit IsLowerBoundEnabledSet(asset, _isLowerBoundEnabled);
    }

    function setIsActionTakingEnabled(address asset, bool _isActionTakingEnabled) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        assetConfigs[asset].isActionTakingEnabled = _isActionTakingEnabled;
        emit IsActionTakingEnabledSet(asset, _isActionTakingEnabled);
    }

    function setLookbackWindowSize(address asset, uint80 _lookbackWindowSize) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (_lookbackWindowSize == 0) revert InvalidLookbackWindow();
        assetConfigs[asset].lookbackWindowSize = _lookbackWindowSize;
        emit LookbackWindowSizeSet(asset, _lookbackWindowSize);
    }

    function setOracle(address asset, address oracle) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (oracle == address(0)) revert ZeroAddress();
        assetConfigs[asset].oracle = oracle;
        emit AssetOracleSet(asset, oracle);
    }

    function deleteAsset(address asset) external onlyUpdater {
        if (!assetConfigs[asset].exists) revert AssetNotFound();

        delete assetConfigs[asset];

        emit AssetDeleted(asset);
    }

    function getParametersForAsset(address asset)
        external
        view
        returns (
            uint64 maxExpectedApy,
            uint32 upperBoundTolerance,
            uint32 lowerBoundTolerance,
            uint32 maxDiscount,
            uint80 lookbackWindowSize,
            bool isUpperBoundEnabled,
            bool isLowerBoundEnabled,
            bool isActionTakingEnabled
        )
    {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        AssetConfig memory config = assetConfigs[asset];

        return (
            config.maxExpectedApy,
            config.upperBoundTolerance,
            config.lowerBoundTolerance,
            config.maxDiscount,
            config.lookbackWindowSize,
            config.isUpperBoundEnabled,
            config.isLowerBoundEnabled,
            config.isActionTakingEnabled
        );
    }

    function getAssetName(address asset) external view returns (string memory) {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        return assetConfigs[asset].name;
    }

    function assetExists(address asset) external view returns (bool) {
        return assetConfigs[asset].exists;
    }

    function getOracle(address asset) external view returns (address) {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        return assetConfigs[asset].oracle;
    }

    function getLookbackData(address asset)
        external
        view
        returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
    {
        if (!assetConfigs[asset].exists) revert AssetNotFound();
        if (assetConfigs[asset].oracle == address(0)) revert OracleNotSet();

        address aggregatorAddress;
        address oracleProxy = assetConfigs[asset].oracle;
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x245a7bfc00000000000000000000000000000000000000000000000000000000) // aggregator() selector
            let success := staticcall(gas(), oracleProxy, ptr, 4, ptr, 32)
            if iszero(success) { revert(0, 0) }
            aggregatorAddress := mload(ptr)
        }

        AggregatorV3Interface aggregator = AggregatorV3Interface(aggregatorAddress);

        // Get the latest round data
        (uint80 latestRoundId,,,,) = aggregator.latestRoundData();

        uint80 lookbackRoundId = latestRoundId <= assetConfigs[asset].lookbackWindowSize
            ? (latestRoundId == 0 ? 0 : 1)
            : latestRoundId - assetConfigs[asset].lookbackWindowSize;

        return aggregator.getRoundData(lookbackRoundId);
    }
}
"
    },
    "node_modules/@openzeppelin/contracts/access/Ownable2Step.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol)

pragma solidity ^0.8.20;

import {Ownable} from "./Ownable.sol";

/**
 * @dev Contract module which provides access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * This extension of the {Ownable} contract includes a two-step mechanism to transfer
 * ownership, where the new owner must call {acceptOwnership} in order to replace the
 * old one. This can help prevent common mistakes, such as transfers of ownership to
 * incorrect accounts, or to contracts that are unable to interact with the
 * permission system.
 *
 * The initial owner is specified at deployment time in the constructor for `Ownable`. This
 * can later be changed with {transferOwnership} and {acceptOwnership}.
 *
 * This module is used through inheritance. It will make available all functions
 * from parent (Ownable).
 */
abstract contract Ownable2Step is Ownable {
    address private _pendingOwner;

    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Returns the address of the pending owner.
     */
    function pendingOwner() public view virtual returns (address) {
        return _pendingOwner;
    }

    /**
     * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one.
     * Can only be called by the current owner.
     *
     * Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer.
     */
    function transferOwnership(address newOwner) public virtual override onlyOwner {
        _pendingOwner = newOwner;
        emit OwnershipTransferStarted(owner(), newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner.
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual override {
        delete _pendingOwner;
        super._transferOwnership(newOwner);
    }

    /**
     * @dev The new owner accepts the ownership transfer.
     */
    function acceptOwnership() public virtual {
        address sender = _msgSender();
        if (pendingOwner() != sender) {
            revert OwnableUnauthorizedAccount(sender);
        }
        _transferOwnership(sender);
    }
}
"
    },
    "node_modules/@openzeppelin/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "node_modules/@openzeppelin/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
      "forge-std/=node_modules/forge-std/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 10000
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "none",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "abi"
        ]
      }
    },
    "evmVersion": "shanghai",
    "viaIR": false
  }
}}

Tags:
Multisig, Multi-Signature, Factory, Oracle|addr:0x69d55d504bc9556e377b340d19818e736bbb318b|verified:true|block:23420158|tx:0xf478881104782ca4de2f2bd05bd233e8ccacfbedaabb81f8f165ce9561cb73a7|first_check:1758566935

Submitted on: 2025-09-22 20:48:55

Comments

Log in to comment.

No comments yet.