GitHub Logo HIP-1215: Generalized Scheduled Contract Calls

Author Matthew DeLorenzo, Michael Tinker
Working Group Richard Bair, Luke Lee, Keith Kowal (, Ali Nikan (, Fernando Paris (, Stanimir Stoyanov
Requested By Hashgraph
Discussions-To https://github.com/hiero-ledger/hiero-improvement-proposals/discussions/1096
Status Last Call
Needs Hedera Review Yes
Needs Hiero Approval Yes
Last Call Period Ends Mon, 14 Jul 2025 07:00:00 +0000
Type Standards Track
Category Service
Created 2025-04-09
Updated 2025-07-23
Requires 756

Abstract

We generalize HIP-756 (“Contract Scheduled Token Create”) by enabling smart contracts to schedule any contract call via Hedera Schedule Service (HSS). The main design concern is throttling; that is, what happens when a contract call is scheduled at an expiry second that is already full of scheduled work. We support cheap retry logic inside the EVM by adding a view system contract hasScheduleCapacity(uint256 expiry, uint256 gasLimit) with the approximate gas cost of a cold SLOAD. This lets the contract find an acceptable second (if one exists) with an affordable gas budget.

Motivation

HIP-755 (“Schedule Service System Contract”) extended the ability to schedule transactions to the Hedera Smart Contract Service. While this is very useful for smart contract calls by externally owned accounts (EOAs), it requires off-chain coordination and it does not extend to smart contracts calling other smart contracts as the origin of the transaction. Furthermore, HIP-755 does not fully support regularly scheduled transactions, as the off-chain signatures and message submission must be repeated for each transaction.

Extending HIP-756 to call contracts in the future would enable recursive execution and rescheduling of contract calls, resulting in a “set it and forget it” system by which contract calls can be made at regularly scheduled intervals as long as the transaction payer has enough HBAR to cover the scheduling and gas and fees.

Rationale

This HIP provides the possiblility to have fully on-chain “cron jobs” that regularly call smart contracts, replacing various off-chain mechanisms users have relied on til now.

User stories

  1. Rebalancing DeFi positions - A vault contract schedules its own future rebalances.
  2. Vesting claims - A token vesting contract automatically schedules cliff unlocks.
  3. DAO automation - Governance logic schedules periodic housekeeping calls (e.g., interest reallocation) without relying on bots.

Specification

HSS System Contract

The IHederaScheduleService.sol interface must be updated to support everything needed to schedule an arbitrary call from within the EVM, including the to address, the call data to use, the gas limit for the future call, and the value to send with that call.

An important option is to let the scheduling contract specify a sender address that may be different from its own. In this case the scheduled call can only execute after receiving enough valid signatures to activate the sender’s key. The difference between scheduleCallWithSender() and executeCallOnSenderSignature() is that the former still waits until the consensus second is not before expirySecond to execute, while the latter executes as soon as the payer signs (unless consensus time is already past the expirySecond, of course).

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Hash        | Function signature                                                                                                                                             |
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 0x6f5bfde8  | scheduleCall(address to, uint256 expirySecond, uint256 gasLimit, uint64 value, bytes memory callData) returns (int64, address)                                 |
| 0xeb459436  | scheduleCallWithSender(address to, address sender, uint256 expirySecond, uint256 gasLimit, uint64 value, bytes memory callData) returns (int64, address)       |
| 0x2c300715  | executeCallOnSenderSignature(address to, address sender, uint256 expirySecond, uint256 gasLimit, uint64 value, bytes memory callData) returns (int64, address) |
| 0x72d42394  | deleteSchedule(address scheduleAddress) returns (int64)                                                                                                        |
| 0xdfb4a999  | hasScheduleCapacity(uint256 expirySecond, uint256 gasLimit) view returns (bool)                                                                                |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

None of these calls ever revert. If scheduleCall() variants fail, they return with a zero address and a int64 failure code whose value is the ordinal of a human-meaningful code in the standard HAPI ResponseCodeEnum. For example, if the requested second is saturated, these calls would return with no created schedule and an int64 that is the protobuf ordinal of enum value "SCHEDULE_EXPIRY_IS_BUSY".

If the scheduleCall() variants succeed, they return the address of the newly created scheduled transaction and 22 as the status code of SUCCESS. A contract may attempt to delete an already scheduled transaction with the deleteSchedule(address), receiving 22 iff the delete succeeds. A contract or EOA may also attempt to delete a scheduled transaction at address 0x00...abcd by calling a “redirect” deleteSchedule() function at that address.

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Hash        | Function signature                                                                                                                                       |
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 0xc61dea85  | deleteSchedule() returns (int64)                                                                                                                         |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------

The hasScheduleCapacity() returns true iff the given second still has capacity to schedule a contract call with the specified gas limit.

The gas cost for scheduling variants will follow Hedera’s standard system contract gas schedule, which is set based on the relative resource usage of the equivalent HAPI ScheduleCreate transaction. This approach remains consistent with existing Hedera prices and ensures predictable fees between system contract and HAPI.

We suggest the following retry pattern to find an available second not before the first second a contract would want a scheduled call to execute. This pattern keeps the simplicity and rapid convergence of exponential back‑off while adding a harmless, non‑manipulable jitter so that many contracts using it to probe the same ideal second naturally scatter across nearby seconds instead of stampeding into the same one.

function findAvailableSecond(
    uint256 expiry,
    uint256 gasLimit,
    uint maxProbes
) returns (uint256 second) {
    if (HSS.hasScheduleCapacity(expiry, gasLimit)) {
        return expiry;
    }
    // Use the PREVRANDAO seed to add jitter to probes
    bytes32 seed = block.prevrandao;
    for (uint i; i < maxProbes; ++i) {
        // 1, 2, 4, 8, ... seconds
        uint256 baseDelay = 1 << i;
        // Jitter, hash the seed with loop‑index
        bytes32 h = keccak256(abi.encodePacked(seed, i));
        // Clamp the lowest 16 bits to [0, min(65536, baseDelay))
        uint16 r = uint16(uint256(h));
        uint256 jitter = uint256(r) % baseDelay;
        uint256 candidate = expiry + baseDelay + jitter;
        if (HSS.hasScheduleCapacity(candidate, gasLimit)) {
            return candidate;
        }
    }
    revert("No unsaturated second after max probes");
}

Backwards Compatibility

No existing features are modified as this only exposes HAPI functionality to smart contracts.

Security Implications

On the surface, one might worry this proposal would let a contract create an infinite loop by scheduling a call to itself at the same second it is currently executing. However, HSS requires the scheduled second to be strictly later than the current consensus second. So it would be strictly more expensive to saturate a Hiero network’s gas throttle by scheduling contract calls than it would be to simply submit those calls through HAPI.

How to Teach This

Smart contracts can now schedule calls to other contracts (or themselves!) from inside the EVM.

Reference Implementation

In progress, see an initial hackathon implementation here.

Rejected Ideas

We considered a proposal to add “schedule not before” semantics to the HAPI ScheduleCreate operation. But since it is relatively trivial to support a cheap hasScheduleCapacity() probe, and it is possible contract authors might want to customize their retry strategies in many ways, we instead settled on providing an example retry strategy and letting contract developers roll their own.

Open Issues

There are no known open issues.

References

  1. HIP-755
  2. HIP-756
  3. HIP-351
  4. ResponseCodeEnum message

Copyright/license

This document is licensed under the Apache License, Version 2.0 — see LICENSE or https://www.apache.org/licenses/LICENSE-2.0.

Citation

Please cite this document as: