> For the complete documentation index, see [llms.txt](https://docs.ipor.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipor.io/build-on-fusion/atomists/vault-configuration-step-by-step/timelocks-and-execution-delays/timelocks-and-execution-delays-developer-reference.md).

# Timelocks and Execution Delays: Developer Reference

This document provides a deep technical breakdown of the implementation, contract interactions, state machine, and programmatic execution of timelocks within the IPOR Fusion protocol. It is intended for smart contract engineers, audit teams, and automation developers. For high-level conceptual guidance, refer to the [Curator Manual](/build-on-fusion/atomists/vault-configuration-step-by-step/timelocks-and-execution-delays.md).

## 1. Smart Contract Architecture

The timelock framework in IPOR Fusion is built natively into `IporFusionAccessManager`, which extends and customizes OpenZeppelin's `AccessManager` contract.

Every restricted function across the IPOR Fusion ecosystem (including `PlasmaVault`, `PlasmaVaultGovernance`, and market connectors) is decorated with the `onlyAuthorized` modifier. This modifier queries the `IporFusionAccessManager` to verify if the caller is authorized and whether they must satisfy an execution delay.

## 2. The Transaction Lifecycle State Machine

A scheduled operation transitions through four distinct state phases inside `IporFusionAccessManager`:

```
          ┌──────────────┐
          │    Unset     │
          └──────┬───────┘
                 │ schedule() called
                 ▼
          ┌──────────────┐
          │   Pending    │ ◄─── block.timestamp < scheduledTime
          └──────┬───────┘
                 │ block.timestamp >= scheduledTime
                 ▼
          ┌──────────────┐
          │    Ready     │ ─── cancel() called ──► [ Unset ]
          └──────┬───────┘
                 │ execute() called
                 ▼
          ┌──────────────┐
          │   Executed   │ (Removed from state / closed)
          └──────────────┘

```

The execution lifecycle of a delayed operation is mathematically bounded by:

$$T\_{scheduled} = T\_{proposal} + D\_{delay}$$

Where:

* $$T\_{proposal}$$ is the block timestamp when `schedule()` is transactionally processed.
* $$D\_{delay}$$ is the configured `executionDelay` of the calling account-role pairing.
* $$T\_{scheduled}$$ is the earliest timestamp at which `execute()` can successfully resolve.

## 3. Operation Hashing & Identification

Every scheduled transaction is tracked using a unique `bytes32` digest called the `operationId`. The `operationId` is computed by hashing the components of the execution target:

$$\text{operationId} = \text{keccak256}(\text{abi.encode}(caller, target, data))$$

Where:

* `caller`: The address initiating the action.
* `target`: The address of the destination contract being called.
* `data`: The exact encoded execution calldata (function selector + parameters).

This hashing mechanism guarantees that an authorized account can only execute the **exact** payload that was originally audited and scheduled. Any alteration to the calldata parameters will result in a completely different `operationId` and fail verification.

## 4. Programmatic Administration: Set, Edit, and Remove

Developers can programmatically manage individual timelocks or set system floors through the access manager's admin interface. Because these actions are protected, they are subject to any execution delays assigned to the administrator address itself.

### A. Setting or Editing an Individual Delay

To set or edit a delay on a specific account, call `grantRole` with the target `executionDelay`:

```
function grantRole(
    uint64 roleId, 
    address account, 
    uint32 executionDelay
) external;
```

* **Behavior**:
  * If the account does not yet possess `roleId`, the role is granted with the specified `executionDelay`.
  * If the account already possesses `roleId`, calling this updates their individual execution delay to the new value.
  * **Emits**: `RoleGranted(roleId, account, executionDelay, newMember)`

### B. Removing an Individual Timelock

To remove a timelock entirely (enabling instant execution), grant the role with a delay of `0`:

```
accessManager.grantRole(Roles.ATOMIST_ROLE, operatorAddress, 0);
```

* **Result**: `operatorAddress` can now directly call Atomist functions on target contracts without going through the `schedule()` workflow.

### C. Restricting Bypasses via Minimal Role Delays

To establish a global safety floor that prevents any administrator from granting zero-delay configurations, set a minimal delay floor:

```
function setMinimalExecutionDelaysForRoles(
    uint64[] calldata roleIds, 
    uint32[] calldata delays
) external;
```

* **Validation**: Once configured, any subsequent call to `grantRole()` that attempts to set an `executionDelay` lower than the registered floor will transactionally revert with the custom error `TooShortExecutionDelayForRole(roleId, executionDelay)`.
* **Emits**: `MinimalExecutionDelaySet(roleId, delay)` per updated role.

## 5. Technical API Reference

### A. Scheduling an Operation

To propose a transaction when subject to a timelock, an account must invoke `schedule`:

```
function schedule(
    address target, 
    bytes calldata data, 
    uint48 when
) external returns (bytes32 operationId, uint32 delay);

```

* **Parameters**:
  * `target`: The destination contract address.
  * `data`: The full encoded payload of the transaction to execute.
  * `when`: The intended execution timestamp. This value must be equal to or greater than $$block.timestamp + D\_{delay}$$.
* **Returns**:
  * `operationId`: The unique cryptographic hash identifying the proposal.
  * `delay`: The exact delay (in seconds) that must elapse before execution.

### B. Executing a Scheduled Operation

Once the delay period has passed, any account can trigger the execution of the scheduled operation by calling `execute`:

```
function execute(
    address target, 
    bytes calldata data
) external payable returns (bytes memory);

```

* **Execution Constraints**:
  * Reverts with `AccessManagerNotReady(operationId)` if $$block.timestamp < T\_{scheduled}$$.
  * Reverts with `AccessManagerExpired(operationId)` if the current time exceeds the expiration threshold ($$T\_{scheduled} + \text{PERIOD\_OF\_GRACE}$$, where the standard Grace Period is typically $$5\text{ days}$$).
  * Executes the payload using a low-level solidity `call`. If the target reverts, `execute` bubbles up the revert data.

### C. Cancelling an Operation

Authorized role administrators or accounts holding the `GUARDIAN_ROLE` can wipe a pending operation from state:

```
function cancel(
    address target, 
    bytes calldata data
) external returns (uint32);

```

* **Behavior**: Clears the execution timestamp of the `operationId` from the storage mapping, reverting its status to `Unset`.
* **Security Guard**: A cancelled operation can never be executed. To run the transaction again, it must be scheduled anew, restarting the entire delay countdown.

### D. Enforcing Safety Floors

The access manager supports establishing minimum execution delays on a per-role basis to prevent administrative oversight or fast-path exploit pathing:

```
function setMinimalExecutionDelaysForRoles(
    uint64[] calldata roleIds, 
    uint32[] calldata delays
) external;

```

* **Constraints**:
  * `roleIds` and `delays` arrays must match in length.
  * Once configured, calling `grantRole()` with an execution delay below the specified floor will revert.

## 6. Security Architecture Notes

#### Self-Referential Timelocks (Admin Constraint)

The administrative capabilities of the `IporFusionAccessManager` are completely governed by its own permission rules. If an administrative account (e.g., `OWNER_ROLE`) has an individual delay, it is strictly bound by that delay when performing administrative actions:

```
// This call will fail if the owner attempts immediate execution:
accessManager.grantRole(Roles.ATOMIST_ROLE, newAtomist, 0);

// Instead, the delayed owner MUST call:
accessManager.schedule(
    address(accessManager),
    abi.encodeWithSelector(accessManager.grantRole.selector, Roles.ATOMIST_ROLE, newAtomist, 0),
    block.timestamp + ownerDelay
);

```

#### Reentrancy & Context Preservation

* To prevent exploitation of state checks, `execute()` implements a reentrancy guard or execution lock during the execution of the call.
* The access manager preserves the execution context using `msg.sender` routing. When a target contract receives a call, it verifies that `msg.sender` is the `IporFusionAccessManager` address, and reads the originating caller context.

## 7. Code Examples & Scripts

#### Scenario: Programmatic Scheduling and Execution of a Performance Fee Update

Below is a hardhat/ethers-style script demonstrating how an automation bot or multisig relayer interacts with the timelocked workflow to update performance fees on a `PlasmaVault` via `PlasmaVaultGovernance`.

```
import { ethers } from "hardhat";

async function main() {
  const [operator] = await ethers.getSigners();

  // Addresses of deployed contracts
  const accessManagerAddress = "0x...AccessManagerAddress";
  const governanceAddress = "0x...PlasmaVaultGovernanceAddress";
  const vaultAddress = "0x...PlasmaVaultAddress";

  // Get contract instances
  const accessManager = await ethers.getContractAt("IporFusionAccessManager", accessManagerAddress);
  const governance = await ethers.getContractAt("PlasmaVaultGovernance", governanceAddress);

  // 1. Prepare the exact calldata payload for the target execution
  const feeAccount = "0x...FeeCollectorAddress";
  const performanceFeeBasisPoints = 1500; // 15.00% performance fee
  
  const targetCalldata = governance.interface.encodeFunctionData("configurePerformanceFee", [
    vaultAddress,
    performanceFeeBasisPoints
  ]);

  console.log("Preparing to schedule fee update...");

  // 2. Fetch the current execution delay of the operator for this role
  const executionDelay = await accessManager.getExecutionDelay(operator.address);
  const blockNumber = await ethers.provider.getBlockNumber();
  const currentBlock = await ethers.provider.getBlock(blockNumber);
  const executionTime = currentBlock.timestamp + Number(executionDelay);

  // 3. Schedule the transaction
  const scheduleTx = await accessManager.schedule(
    governanceAddress,
    targetCalldata,
    executionTime
  );
  const receipt = await scheduleTx.wait();

  // Extract the operationId from the Scheduled event
  const event = receipt.events?.find((e) => e.event === "OperationScheduled");
  const operationId = event?.args?.operationId;
  console.log(`Successfully scheduled. Operation ID: ${operationId}`);
  console.log(`Execution is locked until block timestamp: ${executionTime}`);

  // 4. Fast-forward simulation (Wait Phase)
  // In a real environment, you would wait executionDelay seconds.
  // In tests, you can fast-forward the EVM:
  // await ethers.provider.send("evm_increaseTime", [Number(executionDelay)]);
  // await ethers.provider.send("evm_mine", []);

  // 5. Execute the transaction after the delay has passed
  console.log("Execution delay satisfied. Initiating execution...");
  const executeTx = await accessManager.execute(
    governanceAddress,
    targetCalldata
  );
  await executeTx.wait();
  console.log("Fee configuration successfully executed!");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

```
