> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hedera.com/llms.txt
> Use this file to discover all available pages before exploring further.

# HSS x EVM - Dynamic Rebalancing Through Scheduled Execution (Part 2)

In [Part 1](/evm/tutorials/hedera/hss-evm/part1-schedule-calls), you learned how to schedule future smart contract calls using Hedera's Schedule Service. Now, let's build something more sophisticated: a **capacity-aware DeFi rebalancer** that automatically adjusts its scheduling strategy based on network conditions.

**What makes this advanced?**

Most blockchain automation requires off-chain infrastructure to periodically check and execute operations. Even with Hedera's Schedule Service, naively scheduling all operations at fixed intervals can create network congestion when many contracts compete for the same execution window.

This tutorial demonstrates how to build **intelligent on-chain automation** that:

* **Queries network capacity** before scheduling operations
* **Uses exponential backoff with jitter** to find optimal execution times
* **Self-sustains** by automatically rescheduling after each execution
* **Gracefully handles** network congestion and capacity constraints
* **Supports multiple scheduling methods** (`scheduleCall` and `scheduleCallWithPayer`)
* **Demonstrates one-shot immediate execution** using `executeCallOnPayerSignature`

<Info>
  You can take a look at the **complete code** in the
  [**tutorial-hss-rebalancer-capacity-aware
  repository**](https://github.com/hedera-dev/tutorial-hss-rebalancer-capacity-aware).
</Info>

***

## What You'll Build

A `RebalancerCapacityAware` contract that:

1. **Starts a rebalancing loop** with configurable intervals
2. **Checks network capacity** using `hasScheduleCapacity()` before scheduling
3. **Applies intelligent retry logic** with exponential backoff and randomized jitter
4. **Supports two scheduling methods**: `scheduleCall` and `scheduleCallWithPayer`
5. **Executes rebalances automatically** via scheduled transactions
6. **Reschedules itself** after each execution, creating a self-sustaining loop
7. **Demonstrates one-shot execution** using `executeCallOnPayerSignature`
8. **Can be stopped** by canceling pending scheduled transactions

This pattern is perfect for:

* DeFi vault rebalancing
* Periodic token distributions
* Automated treasury management
* Time-based protocol operations

***

## Prerequisites

* Completion of [Part 1: Schedule Smart Contract Calls](/evm/tutorials/hedera/hss-evm/part1-schedule-calls)
* ECDSA account from the [Hedera Portal](https://portal.hedera.com/)
* Understanding of DeFi rebalancing concepts

***

## Table of Contents <a href="#table-of-contents" id="table-of-contents" />

1. [Setup Project](#setup-project)
2. [Step 1: Understanding the Architecture](#step-1%3A-understanding-the-architecture)
3. [Step 2: Create the Rebalancer Contract](#step-2%3A-create-the-rebalancer-contract)
4. [Step 3: Deploy the Contract](#step-3%3A-deploy-the-contract)
5. [Step 4: Configure the Contract](#step-4%3A-configure-the-contract)
6. [Step 5: Start Rebalancing](#step-5%3A-start-rebalancing)
7. [Step 6: Monitor Rebalancing Operations](#step-6%3A-monitor-rebalancing-operations)
8. [Step 7: Stop Rebalancing](#step-7%3A-stop-rebalancing)
9. [Step 8: One-Shot Immediate Execution (Optional)](#step-8%3A-one-shot-immediate-execution-optional)
10. [Step 9: Run Tests (Optional)](#step-9%3A-run-tests-optional)
11. [Conclusion](#conclusion)
12. [Additional Resources](#additional-resources)

***

## Setup Project

If you completed Part 1, you can use the same project. Otherwise, set up a new project:

```bash theme={null}
mkdir tutorial-hss-rebalancer-capacity-aware
cd tutorial-hss-rebalancer-capacity-aware
npx hardhat --init
```

Make sure to select "**Hardhat 3 -> Typescript Hardhat Project using Mocha and Ethers.js**" and accept the default values. Hardhat will configure your project correctly and install the required dependencies.

<Accordion title="🚧 What's new:   Hardhat 2 → 3 ">
  Key differences in Hardhat 3:

  * **compile → build**\
    `npx hardhat compile` is now `npx hardhat build`. This is the big one. The v3 migration guide explicitly shows using the `build` task.
  * **project init switch**\
    v2 commonly used `npx hardhat` or `npx hardhat init` to bootstrap. In v3 it's `npx hardhat --init`.

  - **keystore helper commands are new**\
    v3's recommended flow includes a keystore plugin with commands like `npx hardhat keystore set HEDERA_RPC_URL` and `npx hardhat keystore set HEDERA_PRIVATE_KEY`. These weren't standard in v2.
  - **Foundry-compatible Solidity tests**\
    In addition to offering Javascript/Typescript integration tests, Hardhat v3 also integrates Foundry-compatible Solidity tests that allows developers to write unit tests directly in Solidity

  * **Enhanced Network Management**\
    v3 allows tasks to create and manage multiple network connections simultaneously which is a significant improvement over the single, fixed connection available in version 2. This provides greater flexibility for scripts and tests that interact with multiple networks.

  *📚 Learn more from the official* [*Hardhat documentation*](https://hardhat.org/docs/getting-started)*.*
</Accordion>

Before we make any changes to our Hardhat configuration file, let's set some configuration variables we will be referring to within the file later.

```bash theme={null}
# If you have already set this before, please use the --force flag
npx hardhat keystore set HEDERA_RPC_URL
```

For `HEDERA_RPC_URL`, we'll have `https://testnet.hashio.io/api`

```bash theme={null}
# If you have already set this before, please use the --force flag
npx hardhat keystore set HEDERA_PRIVATE_KEY
```

For `HEDERA_PRIVATE_KEY`, enter the **HEX Encoded Private Key for your ECDSA account** from the [Hedera Portal. ](https://portal.hedera.com/)

We also need a second private key for testing purposes:

```bash theme={null}
npx hardhat keystore set HEDERA_PRIVATE_KEY_2
```

For `HEDERA_PRIVATE_KEY_2`, enter another **HEX Encoded Private Key for a second ECDSA account**.

Now let's remove the default contracts and scripts that come with the Hardhat project:

```bash theme={null}
rm -rf contracts/* scripts/* test/*
rm -rf ignition
```

#### Install Dependencies

Next, install the required dependencies:

```bash theme={null}
npm install @hiero-ledger/hiero-contracts
```

Note that we are installing the latest code from the main branch when we install `@hiero-ledger/hiero-contracts`. This also gets installed at `@hashgraph/smart-contracts` so we can easily call these contracts from our own contract.

Configure `hardhat.config.ts`:

```typescript hardhat.config.ts theme={null}
import type { HardhatUserConfig } from "hardhat/config";
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable } from "hardhat/config";

const config: HardhatUserConfig = {
  plugins: [hardhatToolboxMochaEthersPlugin],
  solidity: {
    profiles: {
      default: {
        version: "0.8.31"
      },
      production: {
        version: "0.8.31",
        settings: {
          optimizer: {
            enabled: true,
            runs: 200
          }
        }
      }
    }
  },
  networks: {
    testnet: {
      type: "http",
      url: configVariable("HEDERA_RPC_URL"),
      accounts: [configVariable("HEDERA_PRIVATE_KEY")]
    }
  }
};

export default config;
```

***

## Step 1: Understanding the Architecture

Before diving into code, let's understand the key concepts that make this rebalancer capacity-aware.

### The Capacity Problem

When multiple contracts schedule transactions for the same future time:

* Network capacity for that second may be exhausted
* Subsequent scheduling attempts fail
* Operations get delayed or fail entirely

### The Solution: Capacity-Aware Scheduling

Our rebalancer uses three key Hedera features:

**1. hasScheduleCapacity(expirySecond, gasLimit)**

* Queries if a specific future second can accept a scheduled transaction
* Returns `true` if capacity is available, `false` otherwise
* Allows contracts to "probe" future availability

**2. Exponential Backoff with Jitter**

* If desired time lacks capacity, try progressively later times: +1s, +2s, +4s, +8s...
* Add random jitter to avoid "thundering herd" where all contracts retry at the same moment
* Spreads load across multiple seconds

**3. Hedera PRNG System Contract (0x169)**

* Provides pseudorandom seeds for jitter calculation
* Enables true on-chain randomness without external oracles
* Each contract gets different jitter, naturally distributing load

### Scheduling Methods

This tutorial demonstrates three different scheduling approaches:

| Method                        | Use Case                         | Payer    | Loopable |
| ----------------------------- | -------------------------------- | -------- | -------- |
| `scheduleCall`                | Automated recurring operations   | Caller   | ✅ Yes    |
| `scheduleCallWithPayer`       | Recurring with contract as payer | Contract | ✅ Yes    |
| `executeCallOnPayerSignature` | One-shot immediate execution     | Contract | ❌ No     |

<Warning>
  **Important**: `executeCallOnPayerSignature` is **not supported for
  recursive/looped/cron operations** due to Hedera mainnet recursion protection
  (`NO_SCHEDULING_ALLOWED_AFTER_SCHEDULED_RECURSION`). Use `scheduleCall` or
  `scheduleCallWithPayer` for all automated recurring scheduling.
</Warning>

### How It Works Together

```
User calls: startRebalancing(60) // 60-second intervals

Contract:
1. Calculates desired time: now + 60 seconds
2. Checks: hasScheduleCapacity(desiredTime, gasLimit)?
   - YES → Schedule at desiredTime
   - NO → Try exponential backoff with jitter:
     * Try desiredTime + 1 + random(0-1)
     * Try desiredTime + 2 + random(0-2)
     * Try desiredTime + 4 + random(0-4)
     * Try desiredTime + 8 + random(0-8)
     * ...until capacity found or max retries reached

3. Schedule rebalance() at chosen time (using selected method)
4. When rebalance() executes (automatically):
   - Increment counter (or perform real DeFi operation)
   - Calculate next desired time:   now + 60 seconds
   - Repeat capacity-aware scheduling process

Result: Self-sustaining loop that respects network capacity
```

<Note>
  **Why This Matters**

  On traditional EVM chains, you'd need:

  * Off-chain service to monitor network congestion
  * Manual intervention to adjust timing
  * External keeper network that understands capacity

  On Hedera, the **contract itself** is capacity-aware and self-adjusting!
</Note>

***

## Step 2: Create the Rebalancer Contract

Create `RebalancerCapacityAware.sol` in your `contracts` directory:

```solidity contracts/RebalancerCapacityAware.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.31;

import {
    HederaScheduleService
} from "@hashgraph/smart-contracts/contracts/system-contracts/hedera-schedule-service/HederaScheduleService.sol";
import {
    HederaResponseCodes
} from "@hashgraph/smart-contracts/contracts/system-contracts/HederaResponseCodes.sol";
import {
    PrngSystemContract
} from "@hashgraph/smart-contracts/contracts/system-contracts/pseudo-random-number-generator/PrngSystemContract.sol";

contract RebalancerCapacityAware is HederaScheduleService {
    uint256 internal constant REBALANCE_GAS_LIMIT = 2_000_000;

    struct RebalanceConfig {
        bool active;
        uint256 intervalSeconds;
        uint256 lastRebalanceTime;
        uint256 rebalanceCount;
        address lastScheduleAddress;
        address payer;
        bool usePayerScheduling;
    }

    RebalanceConfig public config;

    event RebalancingStarted(
        uint256 intervalSeconds,
        uint256 firstScheduledAt,
        address payer,
        bool usePayerScheduling
    );
    event RebalanceScheduled(
        uint256 chosenTime,
        uint256 desiredTime,
        address scheduleAddress,
        string schedulingMethod
    );
    event RebalanceExecuted(uint256 timestamp, uint256 count);
    event RebalancingStopped();
    event PayerSet(address payer);
    event SchedulingMethodChanged(bool usePayerScheduling);
    event OneShotExecuted(address scheduleAddress, string method);
    event DemoActionExecuted(address caller, uint256 value);

    constructor() payable {}
    receive() external payable {}

    function setPayer(address _payer) external {
        config.payer = _payer;
        emit PayerSet(_payer);
    }

    function setSchedulingMethod(bool _usePayerScheduling) external {
        require(!config.active, "stop rebalancing first");
        config.usePayerScheduling = _usePayerScheduling;
        emit SchedulingMethodChanged(_usePayerScheduling);
    }

    function startRebalancing(uint256 intervalSeconds) external {
        require(intervalSeconds > 0, "interval must be > 0");
        require(!config.active, "already active");

        config.active = true;
        config.intervalSeconds = intervalSeconds;
        config.lastRebalanceTime = block.timestamp;
        config.rebalanceCount = 0;

        uint256 desiredTime = block.timestamp + intervalSeconds;
        uint256 scheduledAt = _scheduleNextRebalance(desiredTime);
        emit RebalancingStarted(
            intervalSeconds,
            scheduledAt,
            config.payer,
            config.usePayerScheduling
        );
    }

    function rebalance() external {
        require(config.active, "not active");
        config.rebalanceCount += 1;
        config.lastRebalanceTime = block.timestamp;

        emit RebalanceExecuted(block.timestamp, config.rebalanceCount);

        uint256 desiredTime = block.timestamp + config.intervalSeconds;
        _scheduleNextRebalance(desiredTime);
    }

    function stopRebalancing() external {
        if (config.lastScheduleAddress != address(0)) {
            address scheduleAddress = config.lastScheduleAddress;
            deleteSchedule(scheduleAddress);
            config.lastScheduleAddress = address(0);
        }
        config.active = false;
        emit RebalancingStopped();
    }

    function _scheduleNextRebalance(
        uint256 desiredTime
    ) internal returns (uint256 chosenTime) {
        chosenTime = _findAvailableSecond(desiredTime, REBALANCE_GAS_LIMIT, 8);
        bytes memory callData = abi.encodeWithSelector(this.rebalance.selector);
        int64 rc;
        address scheduleAddress;
        string memory method;
        if (config.usePayerScheduling && config.payer != address(0)) {
            (rc, scheduleAddress) = scheduleCallWithPayer(
                address(this),
                config.payer,
                chosenTime,
                REBALANCE_GAS_LIMIT,
                0,
                callData
            );
            method = "scheduleCallWithPayer";
        } else {
            (rc, scheduleAddress) = scheduleCall(
                address(this),
                chosenTime,
                REBALANCE_GAS_LIMIT,
                0,
                callData
            );
            method = "scheduleCall";
        }
        require(rc == HederaResponseCodes.SUCCESS, "scheduleCall failed");
        config.lastScheduleAddress = scheduleAddress;
        emit RebalanceScheduled(
            chosenTime,
            desiredTime,
            scheduleAddress,
            method
        );
    }

    function _findAvailableSecond(
        uint256 expiry,
        uint256 gasLimit,
        uint256 maxProbes
    ) internal returns (uint256 second) {
        if (hasScheduleCapacity(expiry, gasLimit)) {
            return expiry;
        }
        bytes32 seed = PrngSystemContract(address(0x169)).getPseudorandomSeed();
        for (uint256 i = 0; i < maxProbes; i++) {
            uint256 baseDelay = 1 << i;
            bytes32 hash = keccak256(abi.encodePacked(seed, i));
            uint16 randomValue = uint16(uint256(hash));
            uint256 jitter = uint256(randomValue) % (baseDelay + 1);

            uint256 candidate = expiry + baseDelay + jitter;
            if (hasScheduleCapacity(candidate, gasLimit)) {
                return candidate;
            }
        }
        revert("No capacity after maxProbes");
    }

    // ----------- One-shot immediate execution demo BEGIN -----------

    function demoImmediateExecution(
        uint256 timestamp,
        bytes memory callData
    ) external returns (address, int64) {
        require(config.payer != address(0), "set payer");
        int64 rc;
        address scheduleAddress;
        (rc, scheduleAddress) = executeCallOnPayerSignature(
            address(this),
            config.payer,
            timestamp,
            REBALANCE_GAS_LIMIT,
            0,
            callData
        );
        emit OneShotExecuted(scheduleAddress, "executeCallOnPayerSignature");
        return (scheduleAddress, rc);
    }

    function demoAction(uint256 value) public {
        emit DemoActionExecuted(msg.sender, value);
    }
    // ----------- One-shot immediate execution demo END -----------

    function getConfig()
        external
        view
        returns (
            bool active,
            uint256 intervalSeconds,
            uint256 lastRebalanceTime,
            uint256 rebalanceCount,
            address lastScheduleAddress,
            address payer,
            bool usePayerScheduling
        )
    {
        return (
            config.active,
            config.intervalSeconds,
            config.lastRebalanceTime,
            config.rebalanceCount,
            config.lastScheduleAddress,
            config.payer,
            config.usePayerScheduling
        );
    }
}
```

<Info>
  **How It Works**

  1. **setPayer()**: Configures which address will pay for scheduled transactions (typically the contract itself)

  2. **setSchedulingMethod()**: Switches between `scheduleCall` (false) and `scheduleCallWithPayer` (true)

  3. **startRebalancing()**: Initializes the loop and schedules the first rebalance using capacity-aware logic

  4. **\_findAvailableSecond()**: The core capacity-awareness algorithm:

     * First checks if desired time has capacity
     * If not, tries exponentially increasing delays: +1s, +2s, +4s, +8s...
     * Adds random jitter (0 to baseDelay) to each attempt
     * Uses Hedera's PRNG for true on-chain randomness

  5. **rebalance()**: Executed automatically by scheduled transactions:

     * Increments counter (in real DeFi, would perform actual rebalancing)
     * Schedules next execution using capacity-aware logic
     * Creates self-sustaining loop

  6. **stopRebalancing()**: Cancels pending schedule and marks loop inactive

  7. **demoImmediateExecution()**: Demonstrates one-shot execution using `executeCallOnPayerSignature`

  8. **HBAR Requirement**: Contract must hold HBAR to pay for all scheduled executions
</Info>

Build the contract:

```bash theme={null}
npx hardhat build
```

***

## Step 3: Deploy the Contract

Create `deploy. ts` in the `scripts` directory:

```typescript scripts/deploy.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with account:", deployer.address);

  const RebalancerCapacityAware = await ethers.getContractFactory(
    "RebalancerCapacityAware",
    deployer
  );

  const contract = await RebalancerCapacityAware.deploy({
    value: ethers.parseEther("20")
  });
  await contract.waitForDeployment();

  const contractAddress = await contract.getAddress();
  console.log("RebalancerCapacityAware deployed at:", contractAddress);

  const balance = await ethers.provider.getBalance(contractAddress);
  console.log("Contract HBAR balance:", ethers.formatEther(balance), "HBAR");

  console.log("📝 Save this address for the next steps!");
  console.log(`export CONTRACT_ADDRESS=${contractAddress}`);
}

main().catch(console.error);
```

Deploy:

```bash theme={null}
npx hardhat run scripts/deploy.ts --network testnet
```

<Check>
  Copy the deployed contract address and set it as an environment variable for
  the next steps.{" "}
</Check>

Expected output:

```bash theme={null}
Deploying with account: 0xe3c0743e01bE37c42B2ee57BD1aA30c9c266c0Ae
RebalancerCapacityAware deployed at: 0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
Contract HBAR balance: 20.0 HBAR
📝 Save this address for the next steps!
export CONTRACT_ADDRESS=0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
```

Set the contract address as an environment variable:

```bash theme={null}
export CONTRACT_ADDRESS=0xYOURDEPLOYEDADDRESS
```

In order to decode events emitted from the contract, the contract must be verified.

```bash theme={null}
./generate_hedera_sc_metadata.sh RebalancerCapacityAware
```

You can then upload the `verify-bundles/RebalancerCapacityAware/metadata.json` file to Hashscan to verify this contract.

***

## Step 4: Configure the Contract

Before starting the rebalancing loop, you need to configure the payer and scheduling method.

### Set the Contract as Payer

Create `setPayer.ts` in the `scripts` directory:

```typescript scripts/setPayer. ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );
  const tx = await rebalancer.setPayer(contractAddress);
  await tx.wait();

  console.log("Payer set to contract address:", contractAddress);
}

main().catch(console.error);
```

Run the script:

```bash theme={null}
npx hardhat run scripts/setPayer.ts --network testnet
```

### Choose a Scheduling Method

You have two options for scheduling. Choose one:

**Option 1: scheduleCall (default)**

Create `setSchedulingMethodScheduleCall.ts`:

```typescript scripts/setSchedulingMethodScheduleCall.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );
  const tx = await rebalancer.setSchedulingMethod(false);
  await tx.wait();

  console.log("Scheduling method set to:  scheduleCall");
}

main().catch(console.error);
```

```bash theme={null}
npx hardhat run scripts/setSchedulingMethodScheduleCall.ts --network testnet
```

**Option 2: scheduleCallWithPayer (contract as payer)**

Create `setSchedulingMethodScheduleCallWithPayer.ts`:

```typescript scripts/setSchedulingMethodScheduleCallWithPayer. ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );
  const tx = await rebalancer.setSchedulingMethod(true);
  await tx.wait();

  console.log(
    "Scheduling method set to: scheduleCallWithPayer (contract as payer)"
  );
}

main().catch(console.error);
```

```bash theme={null}
npx hardhat run scripts/setSchedulingMethodScheduleCallWithPayer.ts --network testnet
```

***

## Step 5: Start Rebalancing

Create `startRebalancing.ts` in the `scripts` directory:

```typescript scripts/startRebalancing.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );
  const intervalSeconds = 15; // set your demo interval here

  const tx = await rebalancer.startRebalancing(intervalSeconds);
  await tx.wait();

  console.log(`Rebalancing started with interval: ${intervalSeconds} seconds`);
}

main().catch(console.error);
```

Run the script:

```bash theme={null}
npx hardhat run scripts/startRebalancing.ts --network testnet
```

Expected output:

```bash theme={null}
Rebalancing started with interval: 15 seconds
```

<Info>
  **What's Happening**

  1. `startRebalancing(15)` calculates desired time: `now + 15 seconds`
  2. Contract checks: `hasScheduleCapacity(desiredTime, 2_000_000)?`
  3. If capacity available → schedules at desired time
  4. If not → applies exponential backoff with jitter to find available slot
  5. Emits `RebalancingStarted` with actual scheduled time and scheduling method
  6. After \~15 seconds, network automatically executes `rebalance()`
  7. `rebalance()` schedules next execution → creates self-sustaining loop
</Info>

***

## Step 6: Monitor Rebalancing Operations

Create `monitorRebalancing. ts` to observe the rebalancing loop:

```typescript scripts/monitorRebalancing.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress
  );

  console.log(
    "Monitoring Rebalancer:",
    contractAddress,
    "\nPress Ctrl+C to stop\n"
  );

  async function display() {
    const config = await rebalancer.getConfig();
    const balance = await ethers.provider.getBalance(contractAddress);
    console.log(`[${new Date().toISOString()}]`);
    console.log("  Active:", config.active);
    console.log("  Rebalance Count:", config.rebalanceCount.toString());
    console.log(
      "  Last Rebalance:",
      config.lastRebalanceTime > 0
        ? new Date(Number(config.lastRebalanceTime) * 1000).toISOString()
        : "Never"
    );
    console.log("  Interval:", config.intervalSeconds.toString(), "seconds");
    console.log("  Payer:", config.payer);
    console.log(
      "  Scheduling Method:",
      config.usePayerScheduling ? "scheduleCallWithPayer" : "scheduleCall"
    );
    console.log("  Contract Balance:", ethers.formatEther(balance), "HBAR");
    console.log("---");
  }

  await display();
  setInterval(display, 5000);
}

main().catch(console.error);
```

Run the monitoring script:

```bash theme={null}
npx hardhat run scripts/monitorRebalancing.ts --network testnet
```

You'll see output like:

```bash theme={null}
Monitoring Rebalancer: 0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
Press Ctrl+C to stop

[2025-12-22T21:09:36.100Z]
  Active: true
  Rebalance Count: 4
  Last Rebalance: 2025-12-22T21:09:25.000Z
  Interval: 15 seconds
  Payer: 0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
  Scheduling Method: scheduleCall
  Contract Balance: 15.2 HBAR
---
[2025-12-22T21:09:41.391Z]
  Active: true
  Rebalance Count: 4
  Last Rebalance: 2025-12-22T21:09:25.000Z
  Interval: 15 seconds
  Payer: 0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
  Scheduling Method: scheduleCall
  Contract Balance: 15.2 HBAR
---
```

Note that the `Rebalance Count` increments every \~15 seconds as scheduled transactions execute automatically. When the contract runs out of HBAR, scheduling will fail, and the count will stop increasing however the state remains `Active: true` until you explicitly stop rebalancing.

### Check Contract Config

You can also create a simple script to check the current configuration:

```typescript scripts/getConfig.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";

  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress
  );

  const config = await rebalancer.getConfig();
  const balance = await ethers.provider.getBalance(contractAddress);

  console.log("Config for contract:", contractAddress);
  console.log({
    active: config.active,
    intervalSeconds: config.intervalSeconds.toString(),
    lastRebalanceTime: config.lastRebalanceTime.toString(),
    rebalanceCount: config.rebalanceCount.toString(),
    lastScheduleAddress: config.lastScheduleAddress,
    payer: config.payer,
    usePayerScheduling: config.usePayerScheduling,
    contractBalance: ethers.formatEther(balance) + " HBAR"
  });
}

main().catch(console.error);
```

```bash theme={null}
npx hardhat run scripts/getConfig.ts --network testnet
```

With output like:

```bash theme={null}
Config for contract: 0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F
{
  active: true,
  intervalSeconds: '15',
  lastRebalanceTime: '1766437807',
  rebalanceCount: '7',
  lastScheduleAddress: '0x00000000000000000000000000000000007294a6',
  payer: '0xFAd66DAA323354799ADF0aF2a019Ce39211bA27F',
  usePayerScheduling: false,
  contractBalance: '11.6 HBAR'
}
```

### View Events on HashScan

Navigate to your contract's events page to see:

**RebalanceScheduled Events:**

```
RebalanceScheduled(
  chosenTime: 1734087330,
  desiredTime: 1734087330,
  scheduleAddress: 0x000000000000000000000000000000000068d3ef,
  schedulingMethod: "scheduleCall"
)
```

* Shows when capacity-aware scheduling found an available slot
* `chosenTime === desiredTime` means ideal time had capacity
* `chosenTime > desiredTime` means backoff was needed
* `schedulingMethod` shows which method was used

**RebalanceExecuted Events:**

```
RebalanceExecuted(
  timestamp: 1734087330,
  count: 1
)
```

* Confirms automatic execution by the network
* Tracks total rebalance operations performed

View live events at: `https://hashscan.io/testnet/contract/$CONTRACT_ADDRESS/events`

***

## Step 7: Stop Rebalancing

Create `stopRebalancing.ts` to halt the loop:

```typescript scripts/stopRebalancing.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );

  const tx = await rebalancer.stopRebalancing();
  await tx.wait();

  console.log("Rebalancing stopped and schedule deleted.");
}

main().catch(console.error);
```

Run:

```bash theme={null}
npx hardhat run scripts/stopRebalancing.ts --network testnet
```

Expected output:

```bash theme={null}
Rebalancing stopped and schedule deleted.
```

<Info>
  **What Happened**

  1. `stopRebalancing()` called `deleteSchedule(lastScheduleAddress)`
  2. Pending scheduled transaction was canceled (best effort)
  3. `config.active` set to `false`
  4. Even if a scheduled `rebalance()` executes, the `require(config.active)` check prevents further scheduling
  5. Loop is fully stopped
</Info>

***

## Step 8: One-Shot Immediate Execution (Optional)

This demo shows how to use `executeCallOnPayerSignature` for a single, immediate function call. This method is **not loopable** due to Hedera's recursion protection.

Create `demoImmediateExecution.ts`:

```typescript scripts/demoImmediateExecution.ts theme={null}
import { network } from "hardhat";
const { ethers } = await network.connect({ network: "testnet" });

async function main() {
  const contractAddress =
    process.env.CONTRACT_ADDRESS || "<your-deployed-contract-address>";
  if (!contractAddress) throw new Error("Set CONTRACT_ADDRESS env var!");
  const [signer] = await ethers.getSigners();

  const rebalancer = await ethers.getContractAt(
    "RebalancerCapacityAware",
    contractAddress,
    signer
  );

  const timestamp = Math.floor(Date.now() / 1000) + 60;
  const callData = rebalancer.interface.encodeFunctionData("demoAction", [
    12345
  ]);

  const tx = await rebalancer.demoImmediateExecution(timestamp, callData);
  const receipt = await tx.wait();

  console.log(
    "One-shot executeCallOnPayerSignature scheduled for",
    new Date(timestamp * 1000).toISOString()
  );
  if (receipt && Array.isArray(receipt.logs)) {
    receipt.logs.forEach((log: any) => {
      try {
        const parsed = rebalancer.interface.parseLog(log);
        if (parsed && parsed.name === "OneShotExecuted") {
          console.log("Schedule Address:", parsed.args.scheduleAddress);
          console.log("Method:", parsed.args.method);
        }
        if (parsed && parsed.name === "DemoActionExecuted") {
          console.log(
            "DemoActionExecuted:  caller",
            parsed.args.caller,
            "value",
            parsed.args.value.toString()
          );
        }
      } catch (_e) {}
    });
  }
}

main().catch(console.error);
```

Make sure the payer is set first, then run:

```bash theme={null}
npx hardhat run scripts/setPayer.ts --network testnet
npx hardhat run scripts/demoImmediateExecution.ts --network testnet
```

With output like:

```bash theme={null}
One-shot executeCallOnPayerSignature scheduled for 2025-12-22T21:13:53.000Z
Schedule Address: 0x00000000000000000000000000000000007294b1
Method: executeCallOnPayerSignature
```

**You should see a `DemoActionExecuted` event emitted.**

***

## Step 9: Run Tests (Optional)

You can find both types of tests in the [tutorial-hss-rebalancer-capacity-aware](https://github.com/hedera-dev/tutorial-hss-rebalancer-capacity-aware) repository. You will find the following files:

The repository includes both Solidity unit tests and TypeScript integration tests.

### Solidity Unit Tests (`contracts/RebalancerCapacityAware.t.sol`)

These tests validate:

* **Initial state**: Verifies contract deploys with inactive configuration
* **Payer configuration**: Tests setting and changing the payer address
* **Scheduling method switching**: Verifies switching between `scheduleCall` and `scheduleCallWithPayer`
* **Start/stop logic**: Confirms only inactive rebalancers can be started and active ones can be stopped
* **Configuration validation**: Ensures interval must be greater than zero
* **HBAR handling**: Verifies contract can receive HBAR for funding scheduled operations
* **State management**: Tests that rebalance count and timestamps are properly maintained

### TypeScript Integration Tests (`test/RebalancerCapacityAware.ts`)

These tests run against Hedera testnet and validate:

* **Deployment and funding**: Deploys with substantial HBAR balance and validates initial state
* **scheduleCall method**: Tests automated recurring rebalancing with `scheduleCall`
* **scheduleCallWithPayer method**: Tests automated recurring rebalancing with `scheduleCallWithPayer` (contract as payer)
* **executeCallOnPayerSignature**: Demonstrates one-shot immediate execution
* **deleteSchedule**: Verifies schedule deletion via `stopRebalancing`
* **Capacity awareness**: Tests that the contract successfully finds available time slots using `hasScheduleCapacity`
* **Input validation**: Tests error handling for invalid inputs
* **Scheduling method switching**: Verifies switching between scheduling methods

Run the tests:

```bash theme={null}
# Solidity unit tests
npx hardhat test solidity

# TypeScript integration tests against testnet
npx hardhat test mocha
```

You can also run both the solidity and mocha tests altogether:

```bash theme={null}
npx hardhat test
```

Which should output something like:

```bash theme={null}
Running Solidity tests

  contracts/RebalancerCapacityAware.t.sol:RebalancerCapacityAwareTest
    ✔ test_SwitchBetweenSchedulingMethods()
    ✔ test_StopRebalancing()
    ✔ test_SetSchedulingMethod()
    ✔ test_SetPayer()
    ✔ test_RevertWhen_SetSchedulingMethodWhileActive()
    ✔ test_RevertWhen_RebalanceNotActive()
    ✔ test_RevertWhen_IntervalIsZero()
    ✔ test_RevertWhen_AlreadyActive()
    ✔ test_ReceiveHBAR()
    ✔ test_PayerConfiguration()
    ✔ test_MultipleStartStopCycles()
    ✔ test_ManualRebalanceIncrementsCount()
    ✔ test_InitialState()
    ✔ test_ConfigUpdatesAfterStart()
    ✔ test_ConfigPersistsAcrossRebalances()

Running Mocha tests

  RebalancerCapacityAware - Comprehensive HSS Demo
Deployer: 0xe3c0743e01bE37c42B2ee57BD1aA30c9c266c0Ae
User: 0xe3c0743e01bE37c42B2ee57BD1aA30c9c266c0Ae
Contract deployed at: 0x2FA345Ad7609bc18d935e48D50F70dB8a1021Fcd
    ✔ should have correct initial state (223ms)
    ✔ should automate recurring rebalancing with scheduleCall (28027ms)
    ✔ should automate recurring rebalancing with scheduleCallWithPayer (contract as payer) (28267ms)
    ✔ should demonstrate executeCallOnPayerSignature as a one-shot (15515ms)
    ✔ should demonstrate deleteSchedule via stopRebalancing (26809ms)
    ✔ should demonstrate hasScheduleCapacity via capacity-aware scheduling (30746ms)
    ✔ should validate input and state transitions (28158ms)
    ✔ should support switching between scheduling methods (22519ms)
    ✔ should support payer configuration (6743ms)


  9 passing (3m)


15 passing (15 solidity)
```

***

## Conclusion

You've built a sophisticated **capacity-aware DeFi rebalancer** that demonstrates advanced patterns with Hedera's Schedule Service!

In this tutorial, you learned how to:

* **Query network capacity** using `hasScheduleCapacity()`
* **Implement exponential backoff** with randomized jitter
* **Use Hedera's PRNG** for true on-chain randomness
* **Build self-sustaining loops** that automatically reschedule
* **Choose between scheduling methods**: `scheduleCall` vs `scheduleCallWithPayer`
* **Handle one-shot execution** using `executeCallOnPayerSignature`
* **Handle network congestion** gracefully
* **Cancel scheduled operations** when needed

### Key Takeaways

* **Capacity-aware scheduling prevents network congestion**. Contracts cooperate with the network's throttling model
* **Exponential backoff + jitter distributes load**. Avoids "thundering herd" where all contracts compete for the same slot
* **True on-chain randomness via PRNG**. No external oracles needed for jitter calculation
* **Multiple scheduling methods for different use cases**. Use `scheduleCall` or `scheduleCallWithPayer` for recurring operations, `executeCallOnPayerSignature` for one-shots
* **This level of network awareness doesn't exist on most EVM chains**. Hedera enables truly intelligent on-chain automation

### Real-World Applications

This pattern can be extended to:

* **DeFi Vaults**: Automatic portfolio rebalancing based on price oracles
* **Liquidity Management**: Periodic adjustment of AMM positions
* **Treasury Operations**: Scheduled fund distributions or buybacks
* **Yield Optimization**: Regular harvesting and compounding of rewards
* **DAO Governance**: Time-delayed execution of approved proposals

All without relying on off-chain infrastructure or keeper networks!

***

## Additional Resources

* [HIP-755: Schedule Service System Contract](https://hips.hedera.com/hip/hip-755)
* [HIP-1215: Generalized Scheduled Contract Calls](https://hips.hedera.com/hip/hip-1215)
* [HIP-351: PRNG System Contract](https://hips.hedera.com/hip/hip-351)
* [Full Contract and Demo Repository](https://github.com/hedera-dev/tutorial-hss-rebalancer-capacity-aware)
* [Part 1: Schedule Smart Contract Calls](/evm/tutorials/hedera/hss-evm/part1-schedule-calls)

<Columns cols={2}>
  <Card title="Writer: Kiran Pachhai, Developer Advocate" arrow>
    [GitHub](https://github.com/kpachhai) |
    [LinkedIn](https://www.linkedin.com/in/kiranpachhai/)
  </Card>
</Columns>
