The Catalyst Underwriter is designed to accelerate swaps speed by taking on the settlement risk of the transaction. The underwriting service relies on the Generalised Relayer which is a reference implementation of a relayer that understands Generalised Incentives.
The Catalyst Underwriter relies on having a running instance of the Generalised Relayer.
ℹ️ The running Generalised Relayer does not need to relay swaps. Rather it is only used to gather the AMB message relaying information.
Aside from the npm packages specified within package.json
, the Underwriter relies on Redis for data management. This dependency however is also required by the Generalised Relayer, so no extra work is required to run the Underwriter.
The Underwriter configuration is split into 2 distinct files.
⚠️ The Underwriter will not run without the following configuration files.
Most of the Underwriter configuration is specified within a .yaml
file located at the project's root directory. The configuration file must be named using the config.{$NODE_ENV}.yaml
format according to the environment variable NODE_ENV
of the runtime (e.g. on a production machine where NODE_ENV=production
, the configuration file must be named config.production.yaml
).
The
NODE_ENV
variable should ideally be set on the shell configuration file (i.e..bashrc
or equivalent), but may also be set by prepending it to the launch command, e.g.NODE_ENV=production docker compose up
. For more information see the Node documentation.
The .yaml
configuration file is divided into the following sections:
global
: Defines the global underwriter configuration.- The
privateKey
of the account that will submit the underwrite transactions on all chains must be defined at this point. - Default configuration for the
monitor
,listener
,underwriter
,expirer
andwallet
can also be specified at this point.
- The
ambs
: The AMBs configuration.chains
: Defines the configuration for each of the chains to be supported by the Underwriter.- This includes the
chainId
and therpc
to be used for the chain. - Each chain may override the global services configurations (those defined under the
global
configuration), andamb
configurations.
- This includes the
endpoints
: The Catalyst endpoints of which swaps to underwrite.- For each vault, the
factoryAddress
,interfaceAddress
,incentivesAddress
andvaultTemplates
, must be specified, together with the swap channel mappings (channelsOnDestination
).
- For each vault, the
ℹ️ For a full reference of the configuration file, see
config.example.yaml
.
Hosts and ports specific configuration is set on a .env
file within the project's root directory.
ℹ️ See
.env.example
for the required environment variables.
The simplest way to run the Underwriter is via docker compose
(refer to the Docker documentation for installation instructions). Run the Underwriter with:
docker compose up [-d]
The -d
option detaches the process to the background.
⚠️ With the default configuration, to use Docker for the Underwriter, the Relayer must also be running with Docker. The Underwriterdocker-compose.yaml
configuration attaches to the default network on which the Relayer resides on to allow communication with it, but this configuration may need to be adjusted on some machines. Use thedocker network
command for more information on the available networks, or refer to the Docker documentation.
Install the required dependencies with:
pnpm install
- NOTE: The
devDependencies
are required to build the project. If running on a production machine whereNODE_ENV=production
, usepnpm install --prod=false
Make sure that a Generalised Relayer implementation is running, and verify that the port of the active Redis database is correctly set on the .env
configuration file.
Build and start the Underwriter with:
pnpm start
For further insight into the requirements for running the Underwriter see the docker-compose.yaml
file.
The Underwriter is devided into 5 main services: Monitor
, Listener
, Underwriter
, Expirer
and Wallet
. These services work together to get the Catalyst swap events and submit their corresponding underwrites on the destination chain. The services are run in parallel and communicate using Redis. Wherever it makes sense, chains are allocated seperate workers to ensure a chain fault doesn't propagate and impact the performance on other chains.
The Monitor service keeps track of the latest block information for each supported chain by subscribing to the Relayer's Monitor service via a websocket connection. This way the Underwriter can be kept in sync with the Relayer.
The Listener service is responsible for fetching the information of the Catalyst swaps/underwrites, in specific:
- Catalyst Chain Interface events:
SwapUnderwritten
: Signals that a swap has been underwritten.FulfillUnderwrite
: Signals that a swap has arrived, an active underwrite exists for the swap, and the underwrite logic has completed.ExpireUnderwrite
: Signals that an underwrite has been expired.
- AMB messages:
- The Underwriter listens at the AMB messages processed by the Relayer, and filters any relevant message that may involve an underwritable swap. These are then further processed and stored, to be later handled by the Underwriter service.
The information gathered with these events is sent to the common Redis database for later use by the other services.
The Underwriter service gets recently executed swap information from Redis. For every new swap, the underwriter:
- Verifies that the swap was executed by a supported set of contracts (token, vault, interface, factory).
- Estimates the token amount required for underwriting.
- Simulates the transaction to get a gas estimate and evaluates the underwriting profitability taking into account the message relaying costs.
- Performs the underwrite if the evaluation is successful using the
underwriteAndCheckConnection
method of the CatalystChainInterface contract via the Wallet service. - Confirms that the underwrite transaction is mined.
To make the Underwriter as resilitent as possible to RPC failures/connection errors, each evaluation, underwrite and confirmation step is tried up to maxTries
times with a retryInterval
delay between tries (these default to 3
and 2000
ms, but can be modified on the Underwriter config).
The Underwriter additionally limits the maximum number of transactions within the 'submission' pipeline (i.e. transactions that have been started to be processed and are not completed), and will not accept any further underwrite orders once reached.
The expirer objective is to resolve any expired underwrites. For underwrites made by this underwriter, the expiry is executed at a configurable expireBlocksMargin
interval before the expiry deadline. Everytime an underwrite is captured by the listener
service, an expire
order is scheduled by the expirer. If the underwrite is fulfilled, the expire
order is discarded, otherwise it is executed at the effective expiry time.
The Wallet service is used to submit transactions requested by the other services of the Underwriter (the Underwriter and the Expirer at the time of writing). For every transaction request:
-
The transaction fee values are dynamically determined according to the following configuration:
- The
maxFeePerGas
configuration sets the transactionmaxFeePerGas
property. This defines the maximum fee to be paid per gas for a transaction (including both the base fee and the miner fee). If not set, nomaxFeePerGas
is set on the transaction. - The
maxPriorityFeeAdjustmentFactor
determines the amount by which to modify the queried recommendedmaxPriorityFee
from the rpc. If not set, nomaxPriorityFee
is set on the transaction. - The
maxAllowedPriorityFeePerGas
sets the maximum value thatmaxPriorityFee
may be set to (after applying themaxPriorityFeeAdjustmentFactor
).
- The
gasPriceAdjustmentFactor
determines the amount by which to modify the queried recommendedgasPrice
from the rpc. If not set, nogasPrice
is set on the transaction. - The
maxAllowedGasPrice
sets the maximum value thatgasPrice
may be set to (after applying thegasPriceAdjustmentFactor
).
⚠️ If the above gas configurations are not specified, the transactions will be submitted using theethers
/rpc defaults. - The
-
The transaction is submitted.
-
The transaction confirmation is awaited.
-
If the transaction fails to be mined after a configurable time interval, the transaction is repriced.
- If a transaction does not mine in time (
maxTries * (confirmationTimeout + retryInterval)
approximately), the Wallet will attempt to reprice the transaction by resubmitting the transaction with higher gas price values. The gas prices are adjusted according to thepriorityAdjustmentFactor
configuration. If not set, it defaults to1.1
(i.e +10%).
- If a transaction does not mine in time (
-
If the transaction still fails to be mined, the wallet will attempt at cancelling the transaction.
⚠️ If the Wallet fails to cancel a transaction, the Submitter pipeline will stall and no further orders will be processed until the stuck transaction is resolved.
To take into consideration the different behaviours and characteristics of different chains, a custom Resolver can be specified for each chain. At the time of writing, the Resolvers can:
- Map the rpc block number to the one observed by the transactions itself (for chains like Arbitrum).
- Estimate gas parameters for transactions, including estimating the gas usage as observed by the transactions (for chains like Arbitrum) and additional L1 fees (for op-stack chains).
ℹ️ Resolvers have to be specified on the configuration file for each desired chain. See
src/resolvers
for the available resolvers.
The Underwriter keeps an estimate of the Underwriter account gas/tokens balance for each chain. A warning is emitted to the logs if the gas/tokens balance falls below a configurable threshold (lowGasBalanceWarning
/lowTokenBalanceWarning
in Wei).
The distinct services of the Underwriter communicate with each other using a Redis database. To abstract the Redis implementation away, a helper library, store.lib.ts
, is provided.
Underwriting may be enabled and disabled dynamically by sending a POST
request to the enableUnderwriting
/disableUnderwriting
endpoints of the underwriter. An optional JSON encoded payload may be specified to select the chainIds
to enable/disable.
ℹ️ Underwriting disabling is useful when it is desired to take down the underwriter, as it allows the underwriter to continue to run throughout a 'take-down' period to handle any required expiries.
The Underwriter uses ethers
types for the contracts that it interacts with (e.g. the Catalyst Vault Common contract). These types are generated with the typechain
package using the contract abis (under the abis/
folder) upon installation of the npm
packages. If the contract abis change the types must be regenerated (see the postinstall
script on package.json
).