Security Model

Amish implements defense-in-depth at the smart contract level. This document describes the security mechanisms verified against the codebase.

Access control

The protocol uses layered access control from OpenZeppelin's contracts.

Protocol owner

The AmishHub owner (a multisig) has limited capabilities:

Can do:

  • Pause and unpause operations

  • Assign operational roles to backend operators

  • Recover funds when the system is paused

  • Deploy new markets

Cannot do:

  • Change terms of active loans

  • Seize user collateral outside of protocol rules

  • Modify interest rates on existing debt

  • Create debt without valid Merkle proofs

Role-based access

Two operational roles control critical functions:

Role
Purpose
Assigned to

POST_NEW_ROOTS_ROLE

Advance epochs with new Merkle roots

Backend operators via assignPostNewRootRole()

EXECUTE_TRANSFERS_ROLE

Execute transfers when restricted mode is enabled

Authorized executors

The POST_NEW_ROOTS_ROLE is defined in Constants.sol and controls access to RootsManager.advanceEpoch(). The AmishHub holds DEFAULT_ADMIN_ROLE for all child contracts and delegates role assignment to its owner.

Approved executor whitelist

Only contracts deployed by the AmishHub can trigger token transfers. When a new market deploys, the hub automatically approves the CollateralManager and DebtIssuer for that market:

External contracts cannot call transferFrom on the hub even if they somehow obtained token approvals.

Pause mechanism

A hierarchical pause system allows rapid response to security incidents.

Pause propagation

The AmishHub is the primary pausable contract using OpenZeppelin's PausableUpgradeable. All child contracts (CollateralManager, DebtIssuer, IssuedDebt) inherit pause state from the hub via PausableByFactory:

A single pause() call on the hub immediately halts operations across all markets.

Functions blocked when paused

AmishHub:

  • deployMarket() - Cannot create new markets

  • transferFrom() - Cannot transfer funds through the hub

  • assignPostNewRootRole() - Cannot assign roles

CollateralManager:

  • executeTransfersWithStoredRoot() - Cannot execute transfers

  • topUpCollateral() - Cannot add collateral to loans

DebtIssuer:

  • executeTransfersWithStoredRoot() - Cannot execute transfers

  • issueDebt() - Cannot issue new debt

  • triggerTransferFromHub() - Cannot trigger hub transfers

IssuedDebt:

  • liquidate() - Cannot liquidate positions

  • repay() - Cannot repay debt

  • redeem() - Cannot redeem debt tokens

Functions that work when paused

View functions continue to work:

  • paused() - Query pause status

  • roots() - Read epoch roots

  • currentEpoch - Check current epoch

  • nullifiedTransfers() - Check transfer nullification status

  • Balance queries and other read-only operations

Emergency fund recovery

All four main contracts (AmishHub, CollateralManager, DebtIssuer, IssuedDebt) inherit from FundsRecoverable:

Emergency recovery is only possible when the protocol is paused, preventing accidental or malicious recovery during normal operation.

Reentrancy protection

All contracts that handle fund movements implement reentrancy guards.

Contracts with ReentrancyGuard

  • CollateralManager

  • DebtIssuer

  • MerkleTransferExecutor (base contract)

  • IssuedDebt

Functions with nonReentrant modifier

MerkleTransferExecutor:

  • _executeTransfers()

IssuedDebt:

  • liquidate()

  • repay()

  • redeem()

Checks-effects-interactions pattern

State changes occur before external calls. Example from MerkleTransferExecutor._executeTransfers():

Example from IssuedDebt.liquidate():

Merkle proof verification

All state transitions require cryptographic proof. The backend constructs Merkle trees containing valid operations for each epoch. Users submit proofs to execute operations on-chain.

Debt issuance

DebtIssuer.issueDebt() verifies that the ImmutableDebtRecord exists in the market state Merkle tree:

The ImmutableDebtRecord contains: debt ID, owed token, collateral token, liquidation threshold, interest rate strategy, repayment deadline, original creditor, owed amount, and collateralization chain ID.

Transfers

MerkleTransferExecutor._executeTransfers() verifies that each TransferExecutionIntent exists in the executable transfers root:

Liquidations

IssuedDebt.liquidate() verifies that the Liquidation record exists in the liquidations root:

Null root prevention

Contracts reject execution against uninitialized epochs:

  • Debt issuance: require(epochStateRoot != bytes32(0), "Epoch state root not posted")

  • Transfers: require(root != bytes32(0), "Executable transfers root not posted")

Replay protection

Every operation has a unique identifier that gets nullified after execution.

Transfer nullification

Liquidation nullification

Once nullified, an operation cannot be replayed even if the same data appears in multiple epoch roots.

Immutable loan terms

Once a loan activates, its terms cannot change. The ImmutableDebtRecord struct is hashed to create the debt token's CREATE2 salt. The same debt record always produces the same token address.

Attempting to issue the same debt twice fails because the CREATE2 address is already occupied. The initializer modifier on all initialization functions prevents re-initialization attacks.

Cross-chain security

Cross-chain operations use storage proofs instead of trusted bridges.

Chain ID verification

Each contract stores its deployment chain ID. Cross-chain proofs must reference the correct source chain.

Facts Registry

State transitions are validated through an external Facts Registry that verifies cryptographic proofs:

The registry confirms that claimed state actually exists on the source chain.

Deterministic addressing

CREATE2 deployment ensures the same market has the same contract addresses on all chains. The market identifier is computed from collateral token, principal token, borrowable chain ID, and interest rate strategy. This serves as the CREATE2 salt, allowing contracts to compute expected addresses without cross-chain communication.

Summary

Protection
Implementation

Access control

OpenZeppelin Ownable + AccessControl, role-based permissions

Pause mechanism

Hierarchical via PausableByFactory, single pause halts all operations

Emergency recovery

Only when paused, owner-only

Reentrancy

ReentrancyGuardUpgradeable + CEI pattern

Proof verification

MerkleProof.verify for all state transitions

Replay prevention

Nullification mappings for transfers and liquidations

Immutable terms

CREATE2 address collision prevents duplicate debt

Cross-chain

Facts Registry validation, deterministic addressing

Last updated