Skip to content

Deployment and Initialization

All three contracts are standalone and fully initialised at construction time. No proxy pattern, no upgradability, no separate initialiser functions. Version 1 uses immutable deployment — if a bug is found, redeploy and migrate funds.

No upgradeable proxies

Do not use OpenZeppelin Initializable. It is designed for upgradeable proxies and leaves an unprotected initialize() call if used without one. All contracts are deployed directly with their constructors.


10.1 Constructor: HouseBuffer

constructor(
    address _asset,   // USDC token address. Must be non-zero contract.
    address _owner    // Governance address. Must be non-zero.
)

Post-construction state:

Variable Value
asset _asset
owner _owner
totalBuffer 0
Approved vaults None — call approveVault() after vault deployment

Constructor validates:

require(_asset  != address(0));
require(_owner  != address(0));
require(_asset.code.length > 0);   // must be a deployed contract

Deploy HouseBuffer first. It has no dependency on LiquidityVault. Fund it after vault approval using fund(uint256 assets).


10.2 Constructor: LiquidityVault

constructor(
    address _asset,       // USDC token address. Must be non-zero contract.
    address _houseBuffer, // Deployed HouseBuffer. Must be non-zero contract.
    address _governance,  // Governance multisig. Must be non-zero.
    address _operator,    // Initial operator address. Must be non-zero.
                          // Controls position management. Hot-wallet role.
    address _keeper,      // Initial keeper bot address. Must be non-zero.
                          // Use setKeeper() to rotate later.
    string  _name,        // ERC-20 share token name (e.g. 'Evergreen Vault Share').
    string  _symbol       // ERC-20 share token symbol (e.g. 'EVS').
)

Post-construction state:

Variable Value
asset _asset
houseBuffer _houseBuffer
governance _governance
pendingGovernance address(0)
operator _operator
keeper _keeper
totalShares 0
idleReserve 0
redeemedToday 0
dayStart block.timestamp
nextRequestId 1
nextProcessId 1
positions[0..3] All EMPTY, all fields zero

Constructor validates:

require(_asset       != address(0));
require(_houseBuffer != address(0));
require(_governance  != address(0));
require(_operator    != address(0));
require(_keeper      != address(0));   // keeper(0) breaks processWithdrawals
require(_asset.code.length       > 0);
require(_houseBuffer.code.length > 0);
require(IERC20(_asset).decimals() == 6);   // USDC decimals assertion

keeper and operator must be non-zero at construction

processWithdrawals requires msg.sender == keeper. If keeper == address(0), every call reverts. Similarly, operator == address(0) would lock all position management. Both are set at construction and rotated later via setKeeper() / setOperator() — never deploy with zero.

governance and houseBuffer are fixed at construction. houseBuffer should be declared immutable in Solidity as it never changes. governance is rotatable via the two-step proposeGovernance / acceptGovernance pattern.


10.3 Constructor: MarketAdapter

constructor(
    address _vault,     // LiquidityVault address. Immutable. Must be non-zero contract.
    address _asset,     // USDC token address. Immutable. Must be non-zero contract.
    address _noToken,   // NO share token for this market. Immutable. Must be non-zero contract.
    bytes32 _marketId   // Venue-specific market identifier. Must be non-zero.
)

Post-construction state:

Variable Value Notes
vault _vault immutable
asset _asset immutable
noToken _noToken immutable
marketId _marketId immutable
settled false Written by venue resolution only
settlementPrice 0 Written by venue resolution only

Constructor validates:

require(_vault    != address(0));
require(_asset    != address(0));
require(_noToken  != address(0));
require(_marketId != bytes32(0));
require(_vault.code.length   > 0);
require(_asset.code.length   > 0);
require(_noToken.code.length > 0);

All four address/bytes32 fields are declared immutable. The adapter is a pure execution wrapper for one market and one vault — no mutable governance state.

Access control on restricted functions:

modifier onlyVault() { require(msg.sender == vault); _; }
// Applied to: buyNoShares, sellNoShares, claimSettlement

A new MarketAdapter is deployed for every new market position. The operator deploys it, verifies the constructor arguments on-chain, then passes the address to vault.openPosition(). The vault does not validate adapter constructor arguments — the operator is responsible for deploying adapters from a verified implementation.


10.4 Deployment Order

Follow this exact sequence. No step can be reordered.

Step 1: Deploy HouseBuffer(_asset, _governance)

Step 2: Deploy LiquidityVault(_asset, houseBuffer.address, _governance, _operator, _keeper, _name, _symbol)

Step 3: houseBuffer.approveVault(vault.address)       ← from governance
        Must precede any deposit. HouseBuffer rejects topUpReserve calls
        from unapproved vaults — processWithdrawals would revert.

Step 4: houseBuffer.fund(initialAmount)
        Seed the buffer (recommended: 15% of target TVL).

Step 5: Vault is ready to accept deposits.
        idleReserve = 0, no positions active.

Step 6: Accept deposits until minimum capitalisation threshold is met.
        See Section 10.5.

Step 7: For each initial market:
          a. Deploy MarketAdapter(vault.address, _asset, _noToken, _marketId)
          b. Call vault.openPosition(slotIndex, adapter.address, assets, maturity)
             Note: entryPrice is NOT supplied — it is returned by adapter.buyNoShares()

Step 8: Stagger market entries by ~15 days per slot. See Section 7.4.

Approve vault before any deposit

Step 3 must precede Step 5. If a topup is triggered before approveVault is called, HouseBuffer will reject the vault's topUpReserve call and processWithdrawals will revert.


10.5 Initial Capitalisation Requirements

The vault must not open its first position until both conditions are satisfied:

// Condition 1: idle reserve covers the reserve target
idleReserve >= totalDeposits * reserveTargetBps / 10000
            >= totalDeposits * 15%

// Condition 2: idle reserve covers at least one full daily cap
idleReserve >= aggMarketNAV * dailyCapBps / 10000
            >= totalDeposits * 2%

// At 15% reserve target and 2% daily cap, Condition 1 is the binding constraint.

Minimum recommended deposit before first openPosition call: $100,000

$100,000 deposit:
  deployableCapital = $85,000    (85% available for positions)
  idleReserve       = $15,000    (covers 7.5 days of maximum daily redemptions)
  dailyCap          =  $2,000    (2% of $100,000)

HouseBuffer initial funding:

Recommended: 12–15% of peak target TVL

At $2M TVL:  $240,000–$300,000
At $500K TVL: $60,000–$75,000

10.6 Post-Deployment Verification Checklist

Before accepting the first deposit, verify each of the following on-chain:

  • [ ] vault.asset() returns the correct USDC address
  • [ ] vault.houseBuffer() returns the correct HouseBuffer address
  • [ ] vault.governance() returns the multisig address
  • [ ] vault.operator() returns the operator address (non-zero)
  • [ ] vault.keeper() returns the keeper bot address (non-zero)
  • [ ] houseBuffer.approvedVaults(vault.address) returns true
  • [ ] vault.paused() returns false
  • [ ] vault.nextRequestId() returns 1
  • [ ] vault.nextProcessId() returns 1
  • [ ] vault.totalShares() returns 0
  • [ ] vault.activePositionCount() returns 0
  • [ ] HouseBuffer has been funded with at least the recommended buffer amount

paused() at zero state

At construction, aggMarketNAV == 0 and dailyCap == 0. paused() returns false because neither pause condition is met when the vault is empty.