Source Code
Overview
ETH Balance
0 ETH
Eth Value
$0.00View more zero value Internal Transactions in Advanced View mode
Advanced mode:
Loading...
Loading
Loading...
Loading
Cross-Chain Transactions
Loading...
Loading
Contract Name:
ZCHFSavingsManager
Compiler Version
v0.8.30+commit.73712a01
Optimization Enabled:
Yes with 200 runs
Other Settings:
prague EvmVersion
Contract Source Code (Solidity Standard Json-Input format)
// 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);
}{
"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
- No Contract Security Audit Submitted- Submit Audit Here
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"}]Contract Creation Code
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
Loading...
Loading
Loading...
Loading
Net Worth in USD
$0.00
Net Worth in ETH
0
Multichain Portfolio | 34 Chains
| Chain | Token | Portfolio % | Price | Amount | Value |
|---|
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.