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
Share Calculations
Cash Operations
Loan Management
Validation
# 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
# Process a share repurchase
def repurchase_shares (
self ,
participant_id : str ,
shares : Decimal,
cash_sources : List[ str ]
) -> RepurchaseTransaction:
repurchase_amount = shares * self .current_share_price
# Draw cash from specified sources (waterfall)
cash_drawn = self .cash_ledger.draw_cash(
amount = repurchase_amount,
sources = cash_sources
)
# Remove shares from circulation
self .allocated_shares -= shares
self .total_shares_outstanding -= shares
return RepurchaseTransaction( ... )
# Receive company contribution
def receive_contribution (
self ,
cash_amount : Decimal,
stock_shares : Decimal = 0
):
self .cash_ledger.unallocated_company_contributions += cash_amount
self .unallocated_shares += stock_shares
self .total_shares_outstanding += stock_shares
# Process annual loan payment and release shares
def process_loan_payment (
self ,
loan_id : str ,
principal_payment : Decimal,
interest_payment : Decimal
) -> Decimal:
loan = self .get_loan(loan_id)
# Calculate shares to release
release_percentage = principal_payment / loan.original_principal
shares_to_release = loan.suspense_shares * release_percentage
# Release shares from suspense
loan.suspense_shares -= shares_to_release
loan.principal_balance -= principal_payment
# Add to allocable pool
self .unallocated_shares += shares_to_release
# Pay interest from cash
self .cash_ledger.unallocated_company_contributions -= interest_payment
return shares_to_release
# Validate trust state consistency
def validate ( self ) -> List[ValidationError]:
errors = []
# Check share conservation
total_calculated = (
self .allocated_shares +
self .unallocated_shares +
self .total_suspense_shares()
)
if total_calculated != self .total_shares_outstanding:
errors.append(ValidationError(
"Share conservation violated" ,
f "Expected { self .total_shares_outstanding } , got { total_calculated } "
))
# Check cash non-negativity
if self .cash_ledger.total_cash() < 0 :
errors.append(ValidationError(
"Negative cash balance" ,
f "Total cash: { self .cash_ledger.total_cash() } "
))
# Check loan balances
for loan in self .loans:
if loan.principal_balance < 0 :
errors.append(ValidationError(
f "Negative loan balance: { loan.loan_id } "
))
return errors
Lifecycle Example
Here’s how an ESOPTrust evolves during a simulation year:
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 ,
...
)
)
Company Contribution
# Company contributes $500K
trust.receive_contribution( cash_amount = 500_000 )
# Cash ledger updated:
# unallocated_contributions: 50,000 → 550,000
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)
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
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)
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
TrustCashLedger Cash segregation details
ESOPLoan Loan structure and mechanics
Simulation Core How trust state evolves during processing