ETH Price: $2,067.93 (+10.73%)
 

Overview

ETH Balance

0 ETH

Eth Value

$0.00

More Info

Private Name Tags

Multichain Info

No addresses found
Transaction Hash
Method
Block
From
To
Create Deposits241834542026-01-07 14:39:2330 days ago1767796763IN
0x9e0569f5...c51D0Bf55
0 ETH0.000041230.29334671
Create Deposits236323692025-10-22 10:07:11107 days ago1761127631IN
0x9e0569f5...c51D0Bf55
0 ETH0.000015880.10908028
Create Deposits235409452025-10-09 15:03:11120 days ago1760022191IN
0x9e0569f5...c51D0Bf55
0 ETH0.000293921.97748084

View more zero value Internal Transactions in Advanced View mode

Advanced mode:
Loading...
Loading
Loading...
Loading
Cross-Chain Transactions

Block Transaction Difficulty Gas Used Reward
View All Blocks Produced

Validator Index Block Amount
View All Withdrawals

Transaction Hash Block Value Eth2 PubKey Valid
View All Deposits
Loading...
Loading

Contract Source Code Verified (Exact Match)

Contract Name:
ZCHFSavingsManager

Compiler Version
v0.8.30+commit.73712a01

Optimization Enabled:
Yes with 200 runs

Other Settings:
prague EvmVersion
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {RedemptionLimiter} from "./RedemptionLimiter.sol";

/// @notice Minimal ERC-20 interface used for basic token operations
interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function balanceOf(address who) external view returns (uint256);
}

/// @notice Minimal interface for interacting with the Frankencoin savings module
interface IFrankencoinSavings {
    function save(uint192 amount) external;
    function currentTicks() external view returns (uint64);
    function currentRatePPM() external view returns (uint24);
    function INTEREST_DELAY() external view returns (uint64);
    function withdraw(address target, uint192 amount) external returns (uint256);
}

/// @title ZCHFSavingsManager
/// @notice Manages batch deposits into the Frankencoin Savings Module with delayed interest and fee deduction.
/// @dev Tracks each deposit independently using an identifier, computes accrued interest using the external tick-based system,
/// and deducts a fixed annual fee on interest. Only entities with OPERATOR_ROLE can create/redeem deposits.
/// Funds can only be received by addresses with RECEIVER_ROLE.
/// @author Plusplus AG ([email protected])
/// @custom:security-contact [email protected]
contract ZCHFSavingsManager is AccessControl, ReentrancyGuard, RedemptionLimiter {
    /// @notice Role required to create or redeem deposits
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    /// @notice Role required to receive withdrawn or rescued funds
    bytes32 public constant RECEIVER_ROLE = keccak256("RECEIVER_ROLE");

    /// @notice Annual fee in parts per million (ppm). For example, 12,500 ppm = 1.25% yearly.
    /// @dev Uint24 is used by the savings module for all ppm values.
    uint24 public constant FEE_ANNUAL_PPM = 12_500;

    /// @notice Struct representing a single tracked customer deposit
    /// @dev `ticksAtDeposit` includes a delay to skip initial non-interest-bearing period.
    struct Deposit {
        /// @dev Amount originally deposited into the savings module (principal). Uint192 is used by the savings module for all amount variables.
        uint192 initialAmount;
        /// @dev Block timestamp when the deposit was created. Uint40 is used by the savings module for all timestamps.
        uint40 createdAt;
        /// @dev Tick count (ppm-seconds) at which interest accrual starts for this deposit. Uint64 is used by the savings module for all tick variables.
        uint64 ticksAtDeposit;
    }

    /// @notice Mapping of unique deposit identifiers to deposit metadata
    /// @dev The identifier is a hashed customer ID, to retain pseudonimity on-chain.
    ///      No way to enumerate deposits as it is not needed by the contract logic. Use events or identifier lists.
    mapping(bytes32 => Deposit) public deposits;

    IERC20 public immutable ZCHF;
    IFrankencoinSavings public immutable savingsModule;

    /// @notice Emitted when a new deposit is created
    /// @param identifier Hashed customer ID
    /// @param amount The amount deposited in ZCHF
    event DepositCreated(bytes32 indexed identifier, uint192 amount);

    /// @notice Emitted when a deposit is redeemed
    /// @param identifier Hashed customer ID
    /// @param totalAmount Amount withdrawn (principal + net interest)
    event DepositRedeemed(bytes32 indexed identifier, uint192 totalAmount);

    // ===========================
    // Custom Errors
    // ===========================

    /// @notice Thrown when a deposit with the given identifier already exists
    error DepositAlreadyExists(bytes32 identifier);

    /// @notice Thrown when a deposit with the given identifier is not found
    error DepositNotFound(bytes32 identifier);

    /// @notice Thrown when expected positive amount is given as zero
    error ZeroAmount();

    /// @notice Thrown when transferFrom fails
    error TransferFromFailed(address from, address to, uint256 amount);

    /// @notice Thrown when an address lacks the RECEIVER_ROLE
    error InvalidReceiver(address receiver);

    /// @notice Thrown when input arrays do not match in length or other argument errors occur
    error InvalidArgument();

    /// @notice Thrown when withdrawal from the savings module is not the expected amount
    error UnexpectedWithdrawalAmount();

    /// @notice Initializes the manager and grants initial roles
    /// @dev This contract grants itself RECEIVER_ROLE for internal redemptions.
    /// @param admin Address to receive DEFAULT_ADMIN_ROLE
    /// @param zchfToken Address of the deployed ZCHF token contract
    /// @param savingsModule_ Address of the deployed Frankencoin savings module
    constructor(address admin, address zchfToken, address savingsModule_) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(RECEIVER_ROLE, address(this));
        ZCHF = IERC20(zchfToken);
        savingsModule = IFrankencoinSavings(savingsModule_);

        // Not needed as the savings module is a registered minter with max allowance to move funds
        // ZCHF.approve(address(savingsModule), type(uint256).max);
    }

    /// @notice Sets the daily redemption limit for a user (in ZCHF).
    /// @dev Only callable by DEFAULT_ADMIN_ROLE. See {RedemptionLimiter-_setDailyRedemptionLimit}.
    /// @param user The operator whose limit is being set.
    /// @param dailyLimit The daily quota (in ZCHF) for the rolling window.
    function setDailyLimit(address user, uint192 dailyLimit) external onlyRole(DEFAULT_ADMIN_ROLE) {
        _setDailyRedemptionLimit(user, dailyLimit);
    }

    /// @notice Creates one or more deposits and forwards the total amount to the savings module.
    /// @dev Each deposit is assigned a unique identifier and accrues interest starting after a fixed delay.
    ///      Reverts if any identifier already exists or any amount is zero. Saves once for all.
    /// @param identifiers Unique identifiers for each deposit. Is a hash of the customer ID (must not be reused).
    /// @param amounts Corresponding deposit amounts (must match identifiers length, non-zero)
    /// @param source The address providing the ZCHF. If `address(this)`, will skip pulling funds.
    function createDeposits(bytes32[] calldata identifiers, uint192[] calldata amounts, address source)
        external
        onlyRole(OPERATOR_ROLE)
        nonReentrant
    {
        uint256 len = identifiers.length;
        if (len != amounts.length) revert InvalidArgument();

        uint256 totalAmount;

        // Pre-validate and sum amounts
        for (uint256 i = 0; i < len; ++i) {
            uint192 amt = amounts[i];

            if (amt == 0) revert ZeroAmount();

            totalAmount += amt;
        }

        // Pull funds from source, if applicable
        if (source != address(this)) {
            bool success = ZCHF.transferFrom(source, address(this), totalAmount);
            if (!success) revert TransferFromFailed(source, address(this), totalAmount);
        }

        // In theory, totalAmount can overflow when cast down. This must be an Input error.
        if (totalAmount > type(uint192).max) revert InvalidArgument();

        // Forward to savings module in a single save() call
        savingsModule.save(uint192(totalAmount));

        // Interest starts accruing only after a fixed delay (defined in savings module).
        // Precompute common tick baseline and post-delay snapshot
        uint64 baseTicks = savingsModule.currentTicks();
        uint24 rate = savingsModule.currentRatePPM();
        uint64 delay = savingsModule.INTEREST_DELAY();
        uint64 tickDelay = uint64(uint256(rate) * delay);
        uint64 ticksAtDeposit = baseTicks + tickDelay;
        uint40 ts = uint40(block.timestamp);

        // Record each individual deposit
        for (uint256 i = 0; i < len; ++i) {
            bytes32 id = identifiers[i];
            uint192 amt = amounts[i];

            if (deposits[id].createdAt != 0) revert DepositAlreadyExists(id);
            deposits[id] = Deposit({initialAmount: amt, createdAt: ts, ticksAtDeposit: ticksAtDeposit});

            emit DepositCreated(id, amt);
        }
    }

    /// @notice Redeems a batch of deposits and forwards the total redeemed funds (principal + net interest) to a receiver.
    /// @dev Each deposit is deleted after redemption. The total amount is withdrawn in a single call to the savings module.
    ///      Operators must respect their daily redemption limit.
    /// @param identifiers Unique identifiers (hashed customer IDs) of the deposits to redeem
    /// @param receiver Address that will receive the ZCHF; must have RECEIVER_ROLE
    function redeemDeposits(bytes32[] calldata identifiers, address receiver)
        external
        onlyRole(OPERATOR_ROLE)
        nonReentrant
    {
        if (!hasRole(RECEIVER_ROLE, receiver)) revert InvalidReceiver(receiver);

        uint192 totalAmount;

        // Process each identifier and sum withdrawal amounts
        for (uint256 i = 0; i < identifiers.length; ++i) {
            bytes32 id = identifiers[i];
            Deposit storage deposit = deposits[id];
            uint192 initialAmount = deposit.initialAmount;

            if (initialAmount == 0) revert DepositNotFound(id);

            (, uint192 netInterest) = getDepositDetails(id);
            uint192 totalForDeposit = initialAmount + netInterest;

            emit DepositRedeemed(id, totalForDeposit);

            totalAmount += totalForDeposit;
            delete deposits[id];
        }

        _useMyRedemptionQuota(totalAmount);

        // Withdraw the full amount from savings to receiver and confirm the amount
        // (Savings module will silently return less if not enough available)
        uint256 withdrawn = savingsModule.withdraw(receiver, totalAmount);
        if (withdrawn != totalAmount) revert UnexpectedWithdrawalAmount();
    }

    /// @notice Returns the current principal and net interest for a given deposit
    /// @dev Accrual is calculated from `ticksAtDeposit` to current tick count.
    /// @param identifier The unique identifier of the deposit
    /// @return initialAmount The originally deposited amount (principal)
    /// @return netInterest The interest accrued after fee deduction
    function getDepositDetails(bytes32 identifier) public view returns (uint192 initialAmount, uint192 netInterest) {
        Deposit storage deposit = deposits[identifier];

        initialAmount = deposit.initialAmount;
        if (initialAmount == 0) return (0, 0);

        uint40 createdAt = deposit.createdAt;
        uint64 currentTicks = savingsModule.currentTicks();

        uint64 ticksAtDeposit = deposit.ticksAtDeposit;
        uint64 deltaTicks = currentTicks > ticksAtDeposit ? currentTicks - ticksAtDeposit : 0;

        // Total interest accrued over deposit lifetime (accounts for initial delay via `ticksAtDeposit`)
        uint256 totalInterest = (uint256(deltaTicks) * initialAmount) / 1_000_000 / 365 days;

        // Fee is time-based, not tick-based. Converts elapsed time to tick-equivalent.
        uint256 duration = block.timestamp - createdAt;
        uint256 feeableTicks = duration * FEE_ANNUAL_PPM;

        // Cap the fee to ensure it's never higher than the actual earned ticks
        uint256 feeTicks = feeableTicks < deltaTicks ? feeableTicks : deltaTicks;

        uint256 fee = feeTicks * initialAmount / 1_000_000 / 365 days;

        // Net interest must not be negative
        // The following clamp is not strictly required, since we clamp the feeTicks above.
        // It is still included to be explicit and futureproof.
        netInterest = totalInterest > fee ? uint192(totalInterest - fee) : 0;

        return (initialAmount, netInterest);
    }

    /// @notice Forwards ZCHF to the savings module without creating a tracked deposit.
    /// @dev Useful for correcting underfunding or over-withdrawal. Funds are added on behalf of this contract.
    /// @param source The address from which ZCHF should be pulled. Use `address(this)` if funds are already held.
    /// @param amount The amount of ZCHF to forward. Caller must have OPERATOR_ROLE.
    function addZCHF(address source, uint192 amount) public onlyRole(OPERATOR_ROLE) nonReentrant {
        if (amount == 0) revert ZeroAmount();

        // Pull ZCHF from external source if needed
        if (source != address(this)) {
            bool success = ZCHF.transferFrom(source, address(this), amount);
            if (!success) revert TransferFromFailed(source, address(this), amount);
        }

        // Save on behalf of the contract (untracked)
        savingsModule.save(amount);
    }

    /// @notice Moves funds from the savings module to a receiver, either to collect fees or migrate balances.
    /// @dev Reverts if not enough funds are available in the savings module.
    /// @param receiver Must have RECEIVER_ROLE
    /// @param amount The maximum amount of ZCHF to move
    function moveZCHF(address receiver, uint192 amount) public onlyRole(OPERATOR_ROLE) nonReentrant {
        if (amount == 0) revert ZeroAmount();
        if (!hasRole(RECEIVER_ROLE, receiver)) revert InvalidReceiver(receiver);

        uint256 movedAmount = savingsModule.withdraw(receiver, amount);
        if (movedAmount != amount) revert UnexpectedWithdrawalAmount();
    }

    /// @notice Recovers arbitrary ERC-20 tokens or ETH accidentally sent to this contract
    /// @dev If a token doesn't follow the ERC-20 specs, rescue can not be guaranteed
    /// @param token Address of the token to recover (use zero address for ETH)
    /// @param receiver Must have RECEIVER_ROLE
    /// @param amount The amount to recover
    function rescueTokens(address token, address receiver, uint256 amount)
        public
        onlyRole(OPERATOR_ROLE)
        nonReentrant
    {
        if (amount == 0) revert ZeroAmount();
        if (!hasRole(RECEIVER_ROLE, receiver)) revert InvalidReceiver(receiver);
        if (token == address(0)) {
            payable(receiver).transfer(amount);
        } else {
            IERC20(token).transfer(receiver, amount);
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (access/AccessControl.sol)

pragma solidity ^0.8.20;

import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {IERC165, ERC165} from "../utils/introspection/ERC165.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```solidity
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```solidity
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
 * to enforce additional security measures for this role.
 */
abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address account => bool) hasRole;
        bytes32 adminRole;
    }

    mapping(bytes32 role => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with an {AccessControlUnauthorizedAccount} error including the required role.
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    /// @inheritdoc IERC165
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual returns (bool) {
        return _roles[role].hasRole[account];
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
     * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
     * is missing `role`.
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert AccessControlUnauthorizedAccount(account, role);
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleGranted} event.
     */
    function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleRevoked} event.
     */
    function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `callerConfirmation`.
     *
     * May emit a {RoleRevoked} event.
     */
    function renounceRole(bytes32 role, address callerConfirmation) public virtual {
        if (callerConfirmation != _msgSender()) {
            revert AccessControlBadConfirmation();
        }

        _revokeRole(role, callerConfirmation);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleGranted} event.
     */
    function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
        if (!hasRole(role, account)) {
            _roles[role].hasRole[account] = true;
            emit RoleGranted(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleRevoked} event.
     */
    function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
        if (hasRole(role, account)) {
            _roles[role].hasRole[account] = false;
            emit RoleRevoked(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

/// @title RedemptionLimiter
/// @notice Rolling 24-hour per-user amount limiter (token-bucket / allowance refill).
/// @dev Intended to be inherited by contracts that need simple rate limiting.
/// @author Plusplus AG ([email protected])
/// @custom:security-contact [email protected]
abstract contract RedemptionLimiter {
    /// @notice Per-operator quota for rate-limiting deposit redemptions
    struct RedemptionQuota {
        uint192 availableAmount; // Remaining amount available for redemption
        uint64 lastRefillTime; // Last time the quota was used/refilled
    }

    /// @notice Per-operator redemption quota tracking
    mapping(address => RedemptionQuota) public userRedemptionQuota;
    /// @notice Daily redemption limit per operator (in asset units)
    mapping(address => uint192) public dailyRedemptionLimit;

    /// @notice Emitted when a user's daily limit is (re)configured.
    event DailyRedemptionLimitSet(address indexed user, uint192 dailyLimit);

    /// @notice Thrown when trying to use a quota for a user without a limit set
    error LimitNotSet();
    /// @notice Thrown when trying to consume more than the available quota
    error WithdrawalLimitExceeded();

    /// @notice Configure or update an operator's daily redemption limit and reset their rolling window to full.
    /// @dev Access control is left to the child contract; call this from a role/owner-gated setter.
    /// @param user The operator whose limit is being set.
    /// @param dailyLimit The daily quota (in asset units) for the rolling window.
    function _setDailyRedemptionLimit(address user, uint192 dailyLimit) internal virtual {
        dailyRedemptionLimit[user] = dailyLimit;

        // Initialize/refresh the current window to full capacity.
        RedemptionQuota storage quota = userRedemptionQuota[user];
        quota.availableAmount = dailyLimit;
        quota.lastRefillTime = uint64(block.timestamp);

        emit DailyRedemptionLimitSet(user, dailyLimit);
    }

    /// @notice Consume quota for an arbitrary user
    /// @dev Recomputes the token-bucket refill since the last update, clamps to limit, then deducts.
    ///      Very small time deltas may result in zero refill due to integer division.
    ///      Reverts with {LimitNotSet} if the user's daily limit is zero.
    ///      Reverts with {WithdrawalLimitExceeded} if `amount` exceeds the available quota.
    /// @param user The user whose quota to consume.
    /// @param amount The amount to deduct from the available quota (in asset units).
    function _useRedemptionQuota(address user, uint256 amount) internal virtual {
        uint192 limit = dailyRedemptionLimit[user];
        if (limit == 0) revert LimitNotSet();

        RedemptionQuota memory quota = userRedemptionQuota[user];

        // Refill quota based on time elapsed since last refill
        uint256 nowTs = block.timestamp;
        uint256 timeElapsed = nowTs - quota.lastRefillTime;
        if (timeElapsed > 0) {
            uint256 refillAmount = (limit * timeElapsed) / 1 days;
            uint256 newAvailable = quota.availableAmount + refillAmount;
            if (newAvailable > limit) newAvailable = limit;

            quota.availableAmount = uint192(newAvailable);
            quota.lastRefillTime = uint64(nowTs);
        }

        // Check if enough quota is available
        if (amount > quota.availableAmount) revert WithdrawalLimitExceeded();

        quota.availableAmount -= uint192(amount);
        userRedemptionQuota[user] = quota;
    }

    /// @notice Convenience helper to consume quota for `msg.sender`.
    /// @dev Calls {_useRedemptionQuota} with `user = msg.sender`.
    /// @param amount The amount to deduct from the caller's available quota.
    function _useMyRedemptionQuota(uint256 amount) internal virtual {
        _useRedemptionQuota(msg.sender, amount);
    }

    /// @notice Compute how much a user could redeem right now, including accrued refill.
    /// @dev Off-chain callers should prefer this view; on-chain callers pay read gas only.
    /// @param user The user to query.
    /// @return available The currently available quota amount in asset units.
    function availableRedemptionQuota(address user) external view returns (uint256 available) {
        uint256 limit = dailyRedemptionLimit[user];
        if (limit == 0) return 0;

        RedemptionQuota memory quota = userRedemptionQuota[user];

        // Calculate potential refill based on time elapsed
        uint256 timeElapsed = block.timestamp - quota.lastRefillTime;
        uint256 refillAmount = (limit * timeElapsed) / 1 days;
        available = uint256(quota.availableAmount) + refillAmount;
        if (available > limit) available = limit;

        return available;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (access/IAccessControl.sol)

pragma solidity >=0.8.4;

/**
 * @dev External interface of AccessControl declared to support ERC-165 detection.
 */
interface IAccessControl {
    /**
     * @dev The `account` is missing a role.
     */
    error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);

    /**
     * @dev The caller of a function is not the expected one.
     *
     * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
     */
    error AccessControlBadConfirmation();

    /**
     * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
     *
     * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
     * {RoleAdminChanged} not being emitted to signal this.
     */
    event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);

    /**
     * @dev Emitted when `account` is granted `role`.
     *
     * `sender` is the account that originated the contract call. This account bears the admin role (for the granted role).
     * Expected in cases where the role was granted using the internal {AccessControl-_grantRole}.
     */
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Emitted when `account` is revoked `role`.
     *
     * `sender` is the account that originated the contract call:
     *   - if using `revokeRole`, it is the admin role bearer
     *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
     */
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) external view returns (bool);

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {AccessControl-_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) external view returns (bytes32);

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function grantRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function revokeRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been granted `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `callerConfirmation`.
     */
    function renounceRole(bytes32 role, address callerConfirmation) external;
}

// 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;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/ERC165.sol)

pragma solidity ^0.8.20;

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

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 */
abstract contract ERC165 is IERC165 {
    /// @inheritdoc IERC165
    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)

pragma solidity >=0.4.16;

/**
 * @dev Interface of the ERC-165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[ERC].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

Settings
{
  "remappings": [
    "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
    "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
    "forge-std/=lib/forge-std/src/",
    "halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
    "openzeppelin-contracts/=lib/openzeppelin-contracts/"
  ],
  "optimizer": {
    "enabled": true,
    "runs": 200
  },
  "metadata": {
    "useLiteralContent": false,
    "bytecodeHash": "ipfs",
    "appendCBOR": true
  },
  "outputSelection": {
    "*": {
      "*": [
        "evm.bytecode",
        "evm.deployedBytecode",
        "devdoc",
        "userdoc",
        "metadata",
        "abi"
      ]
    }
  },
  "evmVersion": "prague",
  "viaIR": true
}

Contract Security Audit

Contract ABI

API
[{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"zchfToken","type":"address"},{"internalType":"address","name":"savingsModule_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[{"internalType":"bytes32","name":"identifier","type":"bytes32"}],"name":"DepositAlreadyExists","type":"error"},{"inputs":[{"internalType":"bytes32","name":"identifier","type":"bytes32"}],"name":"DepositNotFound","type":"error"},{"inputs":[],"name":"InvalidArgument","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"}],"name":"InvalidReceiver","type":"error"},{"inputs":[],"name":"LimitNotSet","type":"error"},{"inputs":[],"name":"ReentrancyGuardReentrantCall","type":"error"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"TransferFromFailed","type":"error"},{"inputs":[],"name":"UnexpectedWithdrawalAmount","type":"error"},{"inputs":[],"name":"WithdrawalLimitExceeded","type":"error"},{"inputs":[],"name":"ZeroAmount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint192","name":"dailyLimit","type":"uint192"}],"name":"DailyRedemptionLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"identifier","type":"bytes32"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"DepositCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"identifier","type":"bytes32"},{"indexed":false,"internalType":"uint192","name":"totalAmount","type":"uint192"}],"name":"DepositRedeemed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE_ANNUAL_PPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OPERATOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RECEIVER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ZCHF","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"source","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"addZCHF","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"availableRedemptionQuota","outputs":[{"internalType":"uint256","name":"available","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"identifiers","type":"bytes32[]"},{"internalType":"uint192[]","name":"amounts","type":"uint192[]"},{"internalType":"address","name":"source","type":"address"}],"name":"createDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"dailyRedemptionLimit","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"deposits","outputs":[{"internalType":"uint192","name":"initialAmount","type":"uint192"},{"internalType":"uint40","name":"createdAt","type":"uint40"},{"internalType":"uint64","name":"ticksAtDeposit","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"identifier","type":"bytes32"}],"name":"getDepositDetails","outputs":[{"internalType":"uint192","name":"initialAmount","type":"uint192"},{"internalType":"uint192","name":"netInterest","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"moveZCHF","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"identifiers","type":"bytes32[]"},{"internalType":"address","name":"receiver","type":"address"}],"name":"redeemDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"callerConfirmation","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"rescueTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"savingsModule","outputs":[{"internalType":"contract IFrankencoinSavings","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"uint192","name":"dailyLimit","type":"uint192"}],"name":"setDailyLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"userRedemptionQuota","outputs":[{"internalType":"uint192","name":"availableAmount","type":"uint192"},{"internalType":"uint64","name":"lastRefillTime","type":"uint64"}],"stateMutability":"view","type":"function"}]

60c0346100d557601f6119ea38819003918201601f19168301916001600160401b038311848410176100d9578084926060946040528339810103126100d557610047816100ed565b9061006d610063604061005c602085016100ed565b93016100ed565b9260018055610101565b5061007730610177565b506001600160a01b039081166080521660a05260405161177f908161020b82396080518181816107b601528181610a380152610d62015260a0518181816104420152818161098a01528181610ba50152818161115301526113820152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036100d557565b6001600160a01b0381165f9081525f5160206119ca5f395f51905f52602052604090205460ff16610172576001600160a01b03165f8181525f5160206119ca5f395f51905f5260205260408120805460ff191660011790553391905f51602061198a5f395f51905f528180a4600190565b505f90565b6001600160a01b0381165f9081525f5160206119aa5f395f51905f52602052604090205460ff16610172576001600160a01b03165f8181525f5160206119aa5f395f51905f5260205260408120805460ff191660011790553391907f7a97506be97703960d71e3a118f1850a50b01da6957110e8293eacb08d8c6060905f51602061198a5f395f51905f529080a460019056fe6080806040526004361015610012575f80fd5b5f905f3560e01c90816301ffc9a714611182575080630f5a27a91461113e5780631086968d14610e6757806312d38ef414610d915780631836e0da14610d4d578063248a9ca314610d235780632f2ff15d14610ce657806336568abe14610ca05780633d4dff7b14610c4a57806349d7575d14610afc5780634f3a19bc14610abc5780635f1051261461094757806391d14854146108fe5780639e17403b146108c3578063a217fddf146108a7578063b09c487314610870578063b9f7ea7a1461039e578063bb1884b514610381578063ce3e2a9314610355578063cea9d26f146101e8578063d547741f146101a1578063f4c64501146101585763f5b541a61461011b575f80fd5b3461015557806003193601126101555760206040517f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b9298152f35b80fd5b50346101555760203660031901126101555760409081906001600160a01b0361017f61121b565b1681526002602052205481519060018060c01b038116825260c01c6020820152f35b5034610155576040366003190112610155576101e46004356101c1611205565b906101df6101da825f525f602052600160405f20015490565b6115ef565b6116c9565b5080f35b50346101555760603660031901126101555761020261121b565b9061020b611205565b9160443590610218611580565b610220611627565b8115610346577f7a97506be97703960d71e3a118f1850a50b01da6957110e8293eacb08d8c60608352602083815260408085206001600160a01b0387165f908152925290205460ff161561032a57919283926001600160a01b0316806102ac5750829182918291906001600160a01b031682f1156102a0575b6001805580f35b604051903d90823e3d90fd5b60405163a9059cbb60e01b81526001600160a01b03909216600483015260248201929092529160209183916044918391905af1801561031f576102f0575b50610299565b6103119060203d602011610318575b61030981836112d3565b8101906112f4565b505f6102ea565b503d6102ff565b6040513d84823e3d90fd5b639cfea58360e01b83526001600160a01b038416600452602483fd5b631f2a200560e01b8352600483fd5b503461015557602036600319011261015557602061037961037461121b565b6114ed565b604051908152f35b503461015557806003193601126101555760206040516130d48152f35b5034610155576060366003190112610155576004356001600160401b038111610779576103cf9036906004016111d5565b6024356001600160401b03811161086c576103ee9036906004016111d5565b9290916044356001600160a01b038116908190036107755761040e611580565b610416611627565b84820361077d578590865b83811061082b575030810361078c575b506001600160c01b03811161077d577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316908690823b1561077957604051633f51270560e21b81526001600160c01b039091166004820152818160248183875af1801561031f57610760575b505060405163b079f16360e01b815290602082600481845afa918215610755578792610734575b50604051630353d9bb60e11b815290602082600481845afa9182156106e45788926106ef575b506020600491604051928380926326fb0ec160e11b82525afa9081156106e4576001600160401b039262ffffff848094610536948d916106b5575b5016911661132b565b16911601936001600160401b0385116106a15791936001600160401b0316914264ffffffffff1691865b81811061056f57876001805580f35b61057a818389611267565b3561058e61058983868a611267565b6114cc565b818a52600460205264ffffffffff60408b205460c01c1661068d5760405190606082018281106001600160401b03821117610679578b896001600160401b036001809897968c60407f93a508225c543b162a23eaff3fc3b4d3a03d9a0b8824c8f2a5ff5f1cec3e36e9986020988252848060c01b0316968784528884019283528184019687528a8152600489522091838060c01b0390848060c01b03905116168460c01b8354161782555181549064ffffffffff60c01b9060c01b169064ffffffffff60c01b1916178155019151166001600160401b0319825416179055604051908152a201610560565b634e487b7160e01b8c52604160045260248cfd5b633a233a5760e11b8a52600482905260248afd5b634e487b7160e01b86526011600452602486fd5b6106d7915060203d6020116106dd575b6106cf81836112d3565b81019061130c565b5f61052d565b503d6106c5565b6040513d8a823e3d90fd5b9091506020813d60201161072c575b8161070b602093836112d3565b81010312610728575162ffffff81168103610728579060206104f2565b8780fd5b3d91506106fe565b61074e91925060203d6020116106dd576106cf81836112d3565b905f6104cc565b6040513d89823e3d90fd5b8161076a916112d3565b61077557855f6104a5565b8580fd5b5080fd5b63a9cb9e0d60e01b8652600486fd5b6040516323b872dd60e01b815260048101829052306024820152604481018390526020816064818b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af19081156106e457889161080c575b506104315763653faf8760e11b875260045230602452604452606485fd5b610825915060203d6020116103185761030981836112d3565b5f6107ee565b916001600160c01b03610842610589858a8a611267565b1690811561085d57600191610856916114e0565b9201610421565b631f2a200560e01b8952600489fd5b8380fd5b503461015557602036600319011261015557604061088f60043561134b565b82516001600160c01b03928316815291166020820152f35b5034610155578060031936011261015557602090604051908152f35b503461015557806003193601126101555760206040517f7a97506be97703960d71e3a118f1850a50b01da6957110e8293eacb08d8c60608152f35b503461015557604036600319011261015557604061091a611205565b91600435815280602052209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b5034610a0a5761095636611231565b61095e611580565b610966611627565b6001600160c01b0316908115610aad576001600160a01b0316308103610a0e575b507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690813b15610a0a575f91602483926040519485938492633f51270560e21b845260048401525af180156109ff576109ec575b506001805580f35b6109f891505f906112d3565b5f5f6109e4565b6040513d5f823e3d90fd5b5f80fd5b6040516323b872dd60e01b815260048101829052306024820152604481018390526020816064815f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af19081156109ff575f91610a8e575b506109875763653faf8760e11b5f526004523060245260445260645ffd5b610aa7915060203d6020116103185761030981836112d3565b5f610a70565b631f2a200560e01b5f5260045ffd5b34610a0a576020366003190112610a0a576001600160a01b03610add61121b565b165f526003602052602060018060c01b0360405f205416604051908152f35b34610a0a57610b0a36611231565b610b12611580565b610b1a611627565b6001600160c01b038116918215610aad576001600160a01b0381165f9081527f537b97c4fba7b49558af13553dbe5acca43d0956b3e9a7288c0112b4bf667d62602052604090205460ff1615610c2a57604051630bc8ee2f60e11b81526001600160a01b0390911660048201526001600160c01b03909116602482015260208180604481015b03815f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af19081156109ff575f91610bf8575b5003610be95760018055005b638499538b60e01b5f5260045ffd5b90506020813d602011610c22575b81610c13602093836112d3565b81010312610a0a575182610bdd565b3d9150610c06565b639cfea58360e01b5f9081526001600160a01b0391909116600452602490fd5b34610a0a576020366003190112610a0a576004355f526004602052606060405f206001600160401b03600182549201541664ffffffffff6040519260018060c01b038116845260c01c1660208301526040820152f35b34610a0a576040366003190112610a0a57610cb9611205565b336001600160a01b03821603610cd757610cd5906004356116c9565b005b63334bd91960e11b5f5260045ffd5b34610a0a576040366003190112610a0a57610cd5600435610d05611205565b90610d1e6101da825f525f602052600160405f20015490565b611647565b34610a0a576020366003190112610a0a5760206103796004355f525f602052600160405f20015490565b34610a0a575f366003190112610a0a576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610a0a57610d9f36611231565b335f9081527fad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5602052604090205490919060ff1615610e50576001600160a01b03165f81815260036020908152604080832080546001600160c01b03969096166001600160c01b0319968716811790915560028352928190204260c01b9095168317909455925190815290917fb1b39ac1746d21933aae33a750bdd3d324646f588f52db8c33769a7cec9b236f91a2005b63e2517d3f60e01b5f52336004525f60245260445ffd5b34610a0a576040366003190112610a0a576004356001600160401b038111610a0a57610e979036906004016111d5565b90610ea0611205565b610ea8611580565b610eb0611627565b6001600160a01b0381165f9081527f537b97c4fba7b49558af13553dbe5acca43d0956b3e9a7288c0112b4bf667d62602052604090205460ff1615610c2a57825f925f915b80831061108c575050335f908152600360205260409020546001600160c01b038481169491169150811561107d57335f52600260205260405f209260405193610f3d856112a4565b546001600160c01b038116855260c01c602085018181529390610f60904261133e565b80611028575b505083516001600160c01b031685116110195783516001600160c01b0390811686900393908411611005576001600160c01b039384168552335f9081526002602090815260409091209551915160c01b6001600160c01b0319169190941617909355610ba092604051630bc8ee2f60e11b81526001600160a01b0390911660048201526001600160c01b03909116602482015291829081906044820190565b634e487b7160e01b5f52601160045260245ffd5b6327351b4b60e11b5f5260045ffd5b6201518061103961104d928461132b565b8751919004906001600160c01b03166114e0565b90808211611075575b506001600160c01b03168452426001600160401b031683528580610f66565b905086611056565b631200806d60e01b5f5260045ffd5b9092919361109b858386611267565b355f818152600460205260409020549095906001600160c01b0316801561112b576001926110d761110f926110cf8a61134b565b91905061128b565b90887f0a0aea629d19c76268c0d921042665e9445e03fb10de654385bc1de37f7478176020604051888060c01b0386168152a261128b565b955f5260046020525f8260408220828155015501919290610ef5565b8663087c619560e31b5f5260045260245ffd5b34610a0a575f366003190112610a0a576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610a0a576020366003190112610a0a576004359063ffffffff60e01b8216809203610a0a57602091637965db0b60e01b81149081156111c4575b5015158152f35b6301ffc9a760e01b149050836111bd565b9181601f84011215610a0a578235916001600160401b038311610a0a576020808501948460051b010111610a0a57565b602435906001600160a01b0382168203610a0a57565b600435906001600160a01b0382168203610a0a57565b6040906003190112610a0a576004356001600160a01b0381168103610a0a57906024356001600160c01b0381168103610a0a5790565b91908110156112775760051b0190565b634e487b7160e01b5f52603260045260245ffd5b6001600160c01b03918216908216019190821161100557565b604081019081106001600160401b038211176112bf57604052565b634e487b7160e01b5f52604160045260245ffd5b90601f801991011681019081106001600160401b038211176112bf57604052565b90816020910312610a0a57518015158103610a0a5790565b90816020910312610a0a57516001600160401b0381168103610a0a5790565b8181029291811591840414171561100557565b9190820391821161100557565b5f90815260046020526040902080546001600160c01b038116929183156114c25760405163b079f16360e01b8152906020826004817f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165afa9182156109ff575f92611493575b50600101546001600160401b0390811691168181111561148357036001600160401b038111611005576001600160401b03905b6001600160c01b038516911661142264ffffffffff6301e13380620f4240611415868661132b565b04049460c01c164261133e565b906130d48202918083046130d41490151715611005576301e1338092620f424092611455928082101561147c575061132b565b040480821115611476576001600160c01b0391611472919061133e565b1690565b50505f90565b905061132b565b50506001600160401b035f6113ed565b6001600160401b0391925060016114b9839260203d6020116106dd576106cf81836112d3565b939250506113ba565b505090505f905f90565b356001600160c01b0381168103610a0a5790565b9190820180921161100557565b6001600160a01b03165f818152600360205260409020546001600160c01b0316908115611476575f52600260205261156f60405f206201518061155c61155660405193611539856112a4565b546001600160c01b038116855260c01c602085018190524261133e565b8561132b565b91519104906001600160c01b03166114e0565b9080821161157b575090565b905090565b335f9081527fee57cd81e84075558e8fcc182a1f4393f91fc97f963a136e66b7f949a62f319f602052604090205460ff16156115b857565b63e2517d3f60e01b5f52336004527f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b92960245260445ffd5b5f8181526020818152604080832033845290915290205460ff16156116115750565b63e2517d3f60e01b5f523360045260245260445ffd5b600260015414611638576002600155565b633ee5aeb560e01b5f5260045ffd5b5f818152602081815260408083206001600160a01b038616845290915290205460ff16611476575f818152602081815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b5f818152602081815260408083206001600160a01b038616845290915290205460ff1615611476575f818152602081815260408083206001600160a01b0395909516808452949091528120805460ff19169055339291907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9080a460019056fea264697066735822122044ddd71ea0ee7429af1ad1e7865937097be4ff3bc30b9450d2d1376a87bebd8064736f6c634300081e00332f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d537b97c4fba7b49558af13553dbe5acca43d0956b3e9a7288c0112b4bf667d62ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb500000000000000000000000011f643abb2a5cddc2381c77a72df9696a25417d6000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b38

Deployed Bytecode

0x6080806040526004361015610012575f80fd5b5f905f3560e01c90816301ffc9a714611182575080630f5a27a91461113e5780631086968d14610e6757806312d38ef414610d915780631836e0da14610d4d578063248a9ca314610d235780632f2ff15d14610ce657806336568abe14610ca05780633d4dff7b14610c4a57806349d7575d14610afc5780634f3a19bc14610abc5780635f1051261461094757806391d14854146108fe5780639e17403b146108c3578063a217fddf146108a7578063b09c487314610870578063b9f7ea7a1461039e578063bb1884b514610381578063ce3e2a9314610355578063cea9d26f146101e8578063d547741f146101a1578063f4c64501146101585763f5b541a61461011b575f80fd5b3461015557806003193601126101555760206040517f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b9298152f35b80fd5b50346101555760203660031901126101555760409081906001600160a01b0361017f61121b565b1681526002602052205481519060018060c01b038116825260c01c6020820152f35b5034610155576040366003190112610155576101e46004356101c1611205565b906101df6101da825f525f602052600160405f20015490565b6115ef565b6116c9565b5080f35b50346101555760603660031901126101555761020261121b565b9061020b611205565b9160443590610218611580565b610220611627565b8115610346577f7a97506be97703960d71e3a118f1850a50b01da6957110e8293eacb08d8c60608352602083815260408085206001600160a01b0387165f908152925290205460ff161561032a57919283926001600160a01b0316806102ac5750829182918291906001600160a01b031682f1156102a0575b6001805580f35b604051903d90823e3d90fd5b60405163a9059cbb60e01b81526001600160a01b03909216600483015260248201929092529160209183916044918391905af1801561031f576102f0575b50610299565b6103119060203d602011610318575b61030981836112d3565b8101906112f4565b505f6102ea565b503d6102ff565b6040513d84823e3d90fd5b639cfea58360e01b83526001600160a01b038416600452602483fd5b631f2a200560e01b8352600483fd5b503461015557602036600319011261015557602061037961037461121b565b6114ed565b604051908152f35b503461015557806003193601126101555760206040516130d48152f35b5034610155576060366003190112610155576004356001600160401b038111610779576103cf9036906004016111d5565b6024356001600160401b03811161086c576103ee9036906004016111d5565b9290916044356001600160a01b038116908190036107755761040e611580565b610416611627565b84820361077d578590865b83811061082b575030810361078c575b506001600160c01b03811161077d577f00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b386001600160a01b0316908690823b1561077957604051633f51270560e21b81526001600160c01b039091166004820152818160248183875af1801561031f57610760575b505060405163b079f16360e01b815290602082600481845afa918215610755578792610734575b50604051630353d9bb60e11b815290602082600481845afa9182156106e45788926106ef575b506020600491604051928380926326fb0ec160e11b82525afa9081156106e4576001600160401b039262ffffff848094610536948d916106b5575b5016911661132b565b16911601936001600160401b0385116106a15791936001600160401b0316914264ffffffffff1691865b81811061056f57876001805580f35b61057a818389611267565b3561058e61058983868a611267565b6114cc565b818a52600460205264ffffffffff60408b205460c01c1661068d5760405190606082018281106001600160401b03821117610679578b896001600160401b036001809897968c60407f93a508225c543b162a23eaff3fc3b4d3a03d9a0b8824c8f2a5ff5f1cec3e36e9986020988252848060c01b0316968784528884019283528184019687528a8152600489522091838060c01b0390848060c01b03905116168460c01b8354161782555181549064ffffffffff60c01b9060c01b169064ffffffffff60c01b1916178155019151166001600160401b0319825416179055604051908152a201610560565b634e487b7160e01b8c52604160045260248cfd5b633a233a5760e11b8a52600482905260248afd5b634e487b7160e01b86526011600452602486fd5b6106d7915060203d6020116106dd575b6106cf81836112d3565b81019061130c565b5f61052d565b503d6106c5565b6040513d8a823e3d90fd5b9091506020813d60201161072c575b8161070b602093836112d3565b81010312610728575162ffffff81168103610728579060206104f2565b8780fd5b3d91506106fe565b61074e91925060203d6020116106dd576106cf81836112d3565b905f6104cc565b6040513d89823e3d90fd5b8161076a916112d3565b61077557855f6104a5565b8580fd5b5080fd5b63a9cb9e0d60e01b8652600486fd5b6040516323b872dd60e01b815260048101829052306024820152604481018390526020816064818b7f000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb6001600160a01b03165af19081156106e457889161080c575b506104315763653faf8760e11b875260045230602452604452606485fd5b610825915060203d6020116103185761030981836112d3565b5f6107ee565b916001600160c01b03610842610589858a8a611267565b1690811561085d57600191610856916114e0565b9201610421565b631f2a200560e01b8952600489fd5b8380fd5b503461015557602036600319011261015557604061088f60043561134b565b82516001600160c01b03928316815291166020820152f35b5034610155578060031936011261015557602090604051908152f35b503461015557806003193601126101555760206040517f7a97506be97703960d71e3a118f1850a50b01da6957110e8293eacb08d8c60608152f35b503461015557604036600319011261015557604061091a611205565b91600435815280602052209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b5034610a0a5761095636611231565b61095e611580565b610966611627565b6001600160c01b0316908115610aad576001600160a01b0316308103610a0e575b507f00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b386001600160a01b031690813b15610a0a575f91602483926040519485938492633f51270560e21b845260048401525af180156109ff576109ec575b506001805580f35b6109f891505f906112d3565b5f5f6109e4565b6040513d5f823e3d90fd5b5f80fd5b6040516323b872dd60e01b815260048101829052306024820152604481018390526020816064815f7f000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb6001600160a01b03165af19081156109ff575f91610a8e575b506109875763653faf8760e11b5f526004523060245260445260645ffd5b610aa7915060203d6020116103185761030981836112d3565b5f610a70565b631f2a200560e01b5f5260045ffd5b34610a0a576020366003190112610a0a576001600160a01b03610add61121b565b165f526003602052602060018060c01b0360405f205416604051908152f35b34610a0a57610b0a36611231565b610b12611580565b610b1a611627565b6001600160c01b038116918215610aad576001600160a01b0381165f9081527f537b97c4fba7b49558af13553dbe5acca43d0956b3e9a7288c0112b4bf667d62602052604090205460ff1615610c2a57604051630bc8ee2f60e11b81526001600160a01b0390911660048201526001600160c01b03909116602482015260208180604481015b03815f7f00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b386001600160a01b03165af19081156109ff575f91610bf8575b5003610be95760018055005b638499538b60e01b5f5260045ffd5b90506020813d602011610c22575b81610c13602093836112d3565b81010312610a0a575182610bdd565b3d9150610c06565b639cfea58360e01b5f9081526001600160a01b0391909116600452602490fd5b34610a0a576020366003190112610a0a576004355f526004602052606060405f206001600160401b03600182549201541664ffffffffff6040519260018060c01b038116845260c01c1660208301526040820152f35b34610a0a576040366003190112610a0a57610cb9611205565b336001600160a01b03821603610cd757610cd5906004356116c9565b005b63334bd91960e11b5f5260045ffd5b34610a0a576040366003190112610a0a57610cd5600435610d05611205565b90610d1e6101da825f525f602052600160405f20015490565b611647565b34610a0a576020366003190112610a0a5760206103796004355f525f602052600160405f20015490565b34610a0a575f366003190112610a0a576040517f000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb6001600160a01b03168152602090f35b34610a0a57610d9f36611231565b335f9081527fad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5602052604090205490919060ff1615610e50576001600160a01b03165f81815260036020908152604080832080546001600160c01b03969096166001600160c01b0319968716811790915560028352928190204260c01b9095168317909455925190815290917fb1b39ac1746d21933aae33a750bdd3d324646f588f52db8c33769a7cec9b236f91a2005b63e2517d3f60e01b5f52336004525f60245260445ffd5b34610a0a576040366003190112610a0a576004356001600160401b038111610a0a57610e979036906004016111d5565b90610ea0611205565b610ea8611580565b610eb0611627565b6001600160a01b0381165f9081527f537b97c4fba7b49558af13553dbe5acca43d0956b3e9a7288c0112b4bf667d62602052604090205460ff1615610c2a57825f925f915b80831061108c575050335f908152600360205260409020546001600160c01b038481169491169150811561107d57335f52600260205260405f209260405193610f3d856112a4565b546001600160c01b038116855260c01c602085018181529390610f60904261133e565b80611028575b505083516001600160c01b031685116110195783516001600160c01b0390811686900393908411611005576001600160c01b039384168552335f9081526002602090815260409091209551915160c01b6001600160c01b0319169190941617909355610ba092604051630bc8ee2f60e11b81526001600160a01b0390911660048201526001600160c01b03909116602482015291829081906044820190565b634e487b7160e01b5f52601160045260245ffd5b6327351b4b60e11b5f5260045ffd5b6201518061103961104d928461132b565b8751919004906001600160c01b03166114e0565b90808211611075575b506001600160c01b03168452426001600160401b031683528580610f66565b905086611056565b631200806d60e01b5f5260045ffd5b9092919361109b858386611267565b355f818152600460205260409020549095906001600160c01b0316801561112b576001926110d761110f926110cf8a61134b565b91905061128b565b90887f0a0aea629d19c76268c0d921042665e9445e03fb10de654385bc1de37f7478176020604051888060c01b0386168152a261128b565b955f5260046020525f8260408220828155015501919290610ef5565b8663087c619560e31b5f5260045260245ffd5b34610a0a575f366003190112610a0a576040517f00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b386001600160a01b03168152602090f35b34610a0a576020366003190112610a0a576004359063ffffffff60e01b8216809203610a0a57602091637965db0b60e01b81149081156111c4575b5015158152f35b6301ffc9a760e01b149050836111bd565b9181601f84011215610a0a578235916001600160401b038311610a0a576020808501948460051b010111610a0a57565b602435906001600160a01b0382168203610a0a57565b600435906001600160a01b0382168203610a0a57565b6040906003190112610a0a576004356001600160a01b0381168103610a0a57906024356001600160c01b0381168103610a0a5790565b91908110156112775760051b0190565b634e487b7160e01b5f52603260045260245ffd5b6001600160c01b03918216908216019190821161100557565b604081019081106001600160401b038211176112bf57604052565b634e487b7160e01b5f52604160045260245ffd5b90601f801991011681019081106001600160401b038211176112bf57604052565b90816020910312610a0a57518015158103610a0a5790565b90816020910312610a0a57516001600160401b0381168103610a0a5790565b8181029291811591840414171561100557565b9190820391821161100557565b5f90815260046020526040902080546001600160c01b038116929183156114c25760405163b079f16360e01b8152906020826004817f00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b386001600160a01b03165afa9182156109ff575f92611493575b50600101546001600160401b0390811691168181111561148357036001600160401b038111611005576001600160401b03905b6001600160c01b038516911661142264ffffffffff6301e13380620f4240611415868661132b565b04049460c01c164261133e565b906130d48202918083046130d41490151715611005576301e1338092620f424092611455928082101561147c575061132b565b040480821115611476576001600160c01b0391611472919061133e565b1690565b50505f90565b905061132b565b50506001600160401b035f6113ed565b6001600160401b0391925060016114b9839260203d6020116106dd576106cf81836112d3565b939250506113ba565b505090505f905f90565b356001600160c01b0381168103610a0a5790565b9190820180921161100557565b6001600160a01b03165f818152600360205260409020546001600160c01b0316908115611476575f52600260205261156f60405f206201518061155c61155660405193611539856112a4565b546001600160c01b038116855260c01c602085018190524261133e565b8561132b565b91519104906001600160c01b03166114e0565b9080821161157b575090565b905090565b335f9081527fee57cd81e84075558e8fcc182a1f4393f91fc97f963a136e66b7f949a62f319f602052604090205460ff16156115b857565b63e2517d3f60e01b5f52336004527f97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b92960245260445ffd5b5f8181526020818152604080832033845290915290205460ff16156116115750565b63e2517d3f60e01b5f523360045260245260445ffd5b600260015414611638576002600155565b633ee5aeb560e01b5f5260045ffd5b5f818152602081815260408083206001600160a01b038616845290915290205460ff16611476575f818152602081815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b5f818152602081815260408083206001600160a01b038616845290915290205460ff1615611476575f818152602081815260408083206001600160a01b0395909516808452949091528120805460ff19169055339291907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9080a460019056fea264697066735822122044ddd71ea0ee7429af1ad1e7865937097be4ff3bc30b9450d2d1376a87bebd8064736f6c634300081e0033

Constructor Arguments (ABI-Encoded and is the last bytes of the Contract Creation Code above)

00000000000000000000000011f643abb2a5cddc2381c77a72df9696a25417d6000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b38

-----Decoded View---------------
Arg [0] : admin (address): 0x11F643abB2a5CDDc2381c77A72DF9696A25417D6
Arg [1] : zchfToken (address): 0xB58E61C3098d85632Df34EecfB899A1Ed80921cB
Arg [2] : savingsModule_ (address): 0x27d9AD987BdE08a0d083ef7e0e4043C857A17B38

-----Encoded View---------------
3 Constructor Arguments found :
Arg [0] : 00000000000000000000000011f643abb2a5cddc2381c77a72df9696a25417d6
Arg [1] : 000000000000000000000000b58e61c3098d85632df34eecfb899a1ed80921cb
Arg [2] : 00000000000000000000000027d9ad987bde08a0d083ef7e0e4043c857a17b38


Block Uncle Number Difficulty Gas Used Reward
View All Uncles
Loading...
Loading
Loading...
Loading
Loading...
Loading
[ Download: CSV Export  ]

A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.