Tracking Events
Last updated
Last updated
This section contains hints for how you can implement your own managed service to track Across events throughout their lifecycle. As an alternative, we also host a public managed solution that you can learn more about in the API section.
See API reference for to query the status of a single deposit.
The recommended solution for tracking all Across deposits originating from your integration, for a single user, is to store the user's depositId
and originChainId
from each transaction originating from your app, and then get the status of each via .
Deposit and corresponding fill events are conveniently scraped by a database and available here. The database implementation can be found in this repository.
When a user deposits capital to a SpokePool, a V3FundsDeposited
event is emitted. This event is now emitted for both deposit()
(legacy function interface) and depositV3()
calls. FundsDeposited
, the V2 event, is no longer possible to emit.
This data comprises the new deposit's "RelayData" which is combined with the block.chainId
of the SpokePool that emitted the event to form:
There are two ways to determine whether a deposit has been filled on the destinationChain
:
Each fill of a deposit emits a FilledV3Relay
which emits all of the data that you'd need to construct another V3RelayData
structure (along with the destination chain's block.chainId
). If this relay hash matches the deposit relay hash, then the deposit is considered valid and the filler will receive a refund in the next bundle.
Call the SpokePool.fillStatuses(bytes32): uint256
function passing in the deposit relay hash. The possible fill statuses are:
When an unfilled deposit's fillDeadline
exceeds the destination chain's block.timestamp
, it is no longer fillable--the SpokePool contract on the destination chain will revert on a fillRelay() call.
Expired deposits are technically refunded by the next root bundle proposed to the HubPool containing an expired deposit refund. Root bundles contain bridge events for a set of block ranges (start and end blocks) for each chain supported in Across. Root bundles are proposed optimistically as a batch of Merkle Roots, one of which is a list of refund payments including deposits that expired in the deposit's origin chain block range. Therefore, to know when precisely an expired deposit was refunded requires reconstructing root bundle data. This is a complex task and we're working on making this data available in a hosted service with an API interface.
In the meantime, we believe its accurate to state that expired deposits will be refunded approximately 90 minutes after their fillDeadline
. Expired deposits are sent to the depositor
address on the originChainId
. The 90 minute figure assumes that on average, deposits expire in the middle of a pending root bundle proposal which implies that the next root bundle will contain the expired deposit refund. Because root bundle proposals are validated optimistically and the current challenge period is 60 minutes, then on average it will take 30 minutes for the next root bundle to be proposed containing a refund for the deposit and another 60 minutes for that root bundle to be executed.
Fill ETAs for deposits vary based on route and available filler liquidity. For example, most deposit transactions will not be filled until they probabilistically finalized on the origin chain, where the deposit occurred.
In general, deposits that exceed current filler liquidity on the destination chain will be delayed, and unprofitable deposits (where the difference in the output amount and the input amount is insufficient to cover the filler's transaction and opportunity costs) might never be filled until they expire. Expired deposits will eventually be refunded, as described above.
The Limits endpoint can be called with the deposit information to forecast the fill ETA. If the deposit amount exceeds the maxDepositShortDelay
, then the deposit is likely to have been marked for slow fill. To deterministically check if a deposit has been marked for slow fill, you can call the fillStatuses() function on the destination SpokePool as described above.