Skip to main content

Overview

The ESOPTrust is the central accounting hub for all plan assets and liabilities. It represents the legal entity that holds company stock and cash on behalf of plan participants.
New in v0.2: Enhanced multi-loan support with dedicated suspense share tracking per loan!
All ESOP assets are owned by the trust, not by individual participants directly. The trust allocates shares and cash to individual participant accounts according to plan rules.

Model Structure

class ESOPTrust(BaseModel):
    """
    The ESOP Trust: Central hub for all plan assets.
    """
    # Identification
    trust_id: str
    plan_name: str
    trust_established_date: date
    
    # Cash Management
    cash_ledger: TrustCashLedger
    
    # Share Management
    total_shares_outstanding: Decimal
    allocated_shares: Decimal
    unallocated_shares: Decimal
    
    # Debt Management
    loans: List[ESOPLoan] = []
    
    # Metadata
    current_year: int
    last_valuation_date: date
    current_share_price: Decimal

Key Properties

The TrustCashLedger manages all trust cash, segregated by source:
trust.cash_ledger = TrustCashLedger(
    participant_cash_accounts=125_000,
    unallocated_company_contributions=50_000,
    unallocated_forfeiture_cash=25_000
)

# Total cash available
total_cash = trust.cash_ledger.total_cash()  # 200,000
See: TrustCashLedger Details
Shares are categorized by their allocation status:
# Shares credited to individual participant accounts
trust.allocated_shares = 45_000

# Shares owned by trust but not yet allocated
trust.unallocated_shares = 5_000

# Shares held as collateral for loans (in suspense)
trust.total_suspense_shares() = 30_000  # Sum across all loans

# Total shares must always balance
assert trust.total_shares_outstanding == (
    trust.allocated_shares + 
    trust.unallocated_shares + 
    trust.total_suspense_shares()
)  # 45,000 + 5,000 + 30,000 = 80,000 ✓
Share Conservation Law: Total shares must always equal the sum of allocated, unallocated, and suspense shares. The engine validates this after every processing step.
The trust can have multiple loans, each with dedicated suspense shares:
trust.loans = [
    ESOPLoan(
        loan_id="LOAN_2020",
        principal_balance=1_500_000,
        interest_rate=0.065,
        suspense_shares=15_000
    ),
    ESOPLoan(
        loan_id="LOAN_2023",
        principal_balance=1_200_000,
        interest_rate=0.070,
        suspense_shares=15_000
    )
]

# Total suspense shares across all loans
total_suspense = trust.total_suspense_shares()  # 30,000
See: ESOPLoan Details

Methods & Operations

Core Methods

# Total suspense shares across all loans
def total_suspense_shares(self) -> Decimal:
    return sum(loan.suspense_shares for loan in self.loans)

# Total market value of trust assets
def total_asset_value(self) -> Decimal:
    share_value = self.total_shares_outstanding * self.current_share_price
    cash_value = self.cash_ledger.total_cash()
    return share_value + cash_value

# Shares available for immediate allocation
def allocable_share_pool(self) -> Decimal:
    return self.unallocated_shares

Lifecycle Example

Here’s how an ESOPTrust evolves during a simulation year:
1

Year Start State

trust = ESOPTrust(
    allocated_shares=45_000,
    unallocated_shares=5_000,
    loans=[ESOPLoan(suspense_shares=30_000, ...)],
    cash_ledger=TrustCashLedger(
        unallocated_contributions=50_000,
        ...
    )
)
2

Company Contribution

# Company contributes $500K
trust.receive_contribution(cash_amount=500_000)

# Cash ledger updated:
# unallocated_contributions: 50,000 → 550,000
3

Loan Payment & Share Release

# Pay loan, release shares
shares_released = trust.process_loan_payment(
    loan_id="LOAN_2020",
    principal_payment=200_000,
    interest_payment=100_000
)

# Results:
# - Suspense shares: 30,000 → 28,000
# - Unallocated shares: 5,000 → 7,000
# - Cash: 550,000 → 450,000 (interest paid)
4

Allocate Shares

# Allocate released shares to participants
trust.allocate_shares(shares=7_000, formula='pro_rata')

# Results:
# - Allocated shares: 45,000 → 52,000
# - Unallocated shares: 7,000 → 0
5

Process Repurchases

# Repurchase from terminated participant
trust.repurchase_shares(
    participant_id="EMP042",
    shares=850,
    cash_sources=[
        'unallocated_contributions',
        'unallocated_forfeitures',
        'participant_cash'
    ]
)

# Results:
# - Allocated shares: 52,000 → 51,150
# - Total shares outstanding: 80,000 → 79,150
# - Cash: 450,000 → 365,000 (assuming $100/share)
6

Year End Snapshot

snapshot = trust.create_snapshot(year=2025)
# Immutable record saved to database

Relationship to Other Models

Real-World Example

A typical ESOP Trust state:
acme_esop = ESOPTrust(
    trust_id="TRUST_ACME_001",
    plan_name="Acme Corporation ESOP",
    trust_established_date=date(2020, 1, 1),
    
    # Cash (segregated by source)
    cash_ledger=TrustCashLedger(
        participant_cash_accounts=125_000,
        unallocated_company_contributions=200_000,
        unallocated_forfeiture_cash=35_000
    ),  # Total: $360,000
    
    # Shares
    total_shares_outstanding=100_000,
    allocated_shares=68_000,      # Credited to 150 participants
    unallocated_shares=2_000,     # Available for next allocation
    
    # Loans
    loans=[
        ESOPLoan(
            loan_id="LOAN_2020_INITIAL",
            principal_balance=1_800_000,
            interest_rate=0.065,
            original_principal=3_000_000,
            suspense_shares=30_000
        )
    ],
    
    # Valuation
    current_year=2025,
    last_valuation_date=date(2024, 12, 31),
    current_share_price=Decimal("110.50")
)

# Validate state
errors = acme_esop.validate()
assert len(errors) == 0  # ✓ Consistent state

# Calculate metrics
print(f"Total asset value: ${acme_esop.total_asset_value():,.2f}")
# Output: Total asset value: $11,410,000
#   (100,000 shares * $110.50 + $360,000 cash)

print(f"Shares in suspense: {acme_esop.total_suspense_shares():,.0f}")
# Output: Shares in suspense: 30,000

print(f"Percentage allocated: {acme_esop.allocated_shares / acme_esop.total_shares_outstanding:.1%}")
# Output: Percentage allocated: 68.0%

Best Practices

Always Validate

Call trust.validate() after any state-modifying operation

Preserve History

Create snapshots before and after major operations

Use Methods

Use provided methods rather than directly modifying attributes

Document Assumptions

Log rationale for any manual adjustments

Next Steps