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:
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 marketstransferFrom()- Cannot transfer funds through the hubassignPostNewRootRole()- Cannot assign roles
CollateralManager:
executeTransfersWithStoredRoot()- Cannot execute transferstopUpCollateral()- Cannot add collateral to loans
DebtIssuer:
executeTransfersWithStoredRoot()- Cannot execute transfersissueDebt()- Cannot issue new debttriggerTransferFromHub()- Cannot trigger hub transfers
IssuedDebt:
liquidate()- Cannot liquidate positionsrepay()- Cannot repay debtredeem()- Cannot redeem debt tokens
Functions that work when paused
View functions continue to work:
paused()- Query pause statusroots()- Read epoch rootscurrentEpoch- Check current epochnullifiedTransfers()- Check transfer nullification statusBalance 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
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