Skip to main content
The Stylus contracts use dynamic registries — indexed mappings with soft-delete removal — so new protocols and DEXes can be added on-chain without redeployment. This guide covers both workflows.

Adding a Lending Protocol to LiquidationMonitor

The LiquidationMonitor contract maintains a registry of lending protocols it can scan for at-risk accounts. Each protocol entry has an address, a type constant, and an active flag.

Supported Protocol Types

Type ConstantValueProtocol
LENDING_TYPE_AAVE_V30Aave V3
LENDING_TYPE_COMPOUND_V31Compound V3 (reserved)
1

Define the protocol interface

If the new protocol uses a different interface than Aave V3, add a sol_interface! declaration in contracts/liquidation-monitor/src/lib.rs:
sol_interface! {
    interface ICompoundComet {
        function borrowBalanceOf(address account)
            external view returns (uint256);
        function getAssetInfo(uint8 i)
            external view returns (
                uint8 offset,
                address asset,
                address priceFeed,
                uint64 scale,
                uint64 borrowCollateralFactor,
                uint64 liquidateCollateralFactor,
                uint64 liquidationFactor,
                uint128 supplyCap
            );
    }
}
2

Add a type constant

Define a new constant for the protocol type:
const LENDING_TYPE_COMPOUND_V3: u64 = 1;
The contract dispatches health factor queries based on this type constant, so each protocol needs a unique value.
3

Add dispatch logic in the health scanner

In the get_health_factor method, add a branch for the new type:
if protocol_type == U256::from(LENDING_TYPE_COMPOUND_V3) {
    let comet = ICompoundComet::new(pool_address);
    let borrow_balance = comet.borrow_balance_of(
        self.vm(), Call::new(), account
    )?;
    // Calculate health factor based on Compound V3 logic
    // ...
}
4

Register the protocol on-chain

Call add_lending_protocol with the pool address and type constant:
cast send LIQUIDATION_MONITOR_ADDRESS \
  "add_lending_protocol(address,uint64)" \
  COMPOUND_COMET_ADDRESS 1 \
  --rpc-url https://arb1.arbitrum.io/rpc \
  --private-key 0xOWNER_KEY
This emits a LendingProtocolAdded event and returns the registry index.
5

Write tests

Add tests using the TestVM and mock_static_call patterns:
#[test]
fn test_compound_health_factor() {
    let (vm, mut monitor) = setup();
    vm.set_sender(OWNER);

    // Register Compound V3
    let idx = monitor
        .add_lending_protocol(COMPOUND_ADDR, 1)
        .unwrap();

    // Mock the borrow_balance_of call
    let calldata = /* build calldata */;
    vm.mock_static_call(
        COMPOUND_ADDR, calldata,
        Ok(U256::from(1000).to_be_bytes_vec())
    );

    // Test health factor query
    let hf = monitor.get_health_factor(ALICE).unwrap();
    assert!(hf > U256::ZERO);
}
mock_static_call in stylus-test 0.10.0 always returns the LAST registered mock’s data. Register losing mocks first, then the winning mock last.

Soft-Delete Removal Pattern

Both contracts use soft-delete rather than array compaction. When you remove a protocol or DEX, the active flag is set to false but the entry remains in the mapping. The scanning functions skip inactive entries.
# Remove a lending protocol by index
cast send LIQUIDATION_MONITOR_ADDRESS \
  "remove_lending_protocol(uint256)" 0 \
  --rpc-url https://arb1.arbitrum.io/rpc \
  --private-key 0xOWNER_KEY

# Remove a DEX by index
cast send ROUTE_OPTIMIZER_ADDRESS \
  "remove_dex(uint256)" 0 \
  --rpc-url https://arb1.arbitrum.io/rpc \
  --private-key 0xOWNER_KEY
Removing a protocol emits a LendingProtocolRemoved or DexRemoved event. Attempting to remove an already-inactive entry reverts.
Use get_lending_protocol(index) or get_dex(index) to check the current state of a registry entry before removal.