When an unbonding period completes, the protocol emits an UnbondingCompleted event through the StableSystem precompile (0x0000000000000000000000000000000000009999) via a system transaction. This lets dApps notify users and update balances in real time without running custom indexers or polling REST endpoints.
Concept: For how system transactions bridge SDK-layer events to the EVM and why it matters, see System transactions .
Prerequisites
Understanding of System transactions .
Familiarity with Staking , specifically undelegate and the unbonding process.
Experience with contract event subscription and filtering using a standard web3 library (e.g. ethers.js v6).
Overview
Set up the contract instance : create a contract instance for the StableSystem precompile.
Handle events in your application : subscribe to real-time events or query historical data depending on your application logic.
Handle connection issues : implement reconnection logic for persistent WebSocket subscriptions.
Step 1: Set up the contract instance
Create a contract instance for the StableSystem precompile using the UnbondingCompleted event ABI.
// config.ts
import { ethers } from "ethers" ;
export const STABLE_SYSTEM_ADDRESS =
"0x0000000000000000000000000000000000009999" ;
export const STABLE_SYSTEM_ABI = [
"event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)" ,
];
export const provider = new ethers . JsonRpcProvider ( "https://rpc.testnet.stable.xyz" );
export const stableSystem = new ethers . Contract (
STABLE_SYSTEM_ADDRESS ,
STABLE_SYSTEM_ABI ,
provider
);
Step 2: Handle events in your application
Subscribe to real-time events, query historical data, or both depending on your application logic.
Real-time subscription
Subscribe to UnbondingCompleted events for real-time notifications when any unbonding completes. Useful for triggering balance updates, sending notifications, or refreshing dashboard statistics.
// subscribeBasic.ts
import { stableSystem } from "./config" ;
stableSystem . on ( "UnbondingCompleted" , ( delegator , validator , amount , event ) => {
console . log ( "Unbonding completed:" );
console . log ( " Delegator:" , delegator );
console . log ( " Validator:" , validator );
console . log ( " Amount:" , ethers . formatEther ( amount ), "tokens" );
console . log ( " Block:" , event . log . blockNumber );
console . log ( " Tx Hash:" , event . log . transactionHash );
});
Filter by user
To only receive events for a particular delegator address, use the indexed event parameters to create a filter.
// subscribeByUser.ts
import { ethers } from "ethers" ;
import { stableSystem } from "./config" ;
const userAddress = "0xabcd..." ;
const filter = stableSystem . filters . UnbondingCompleted ( userAddress );
stableSystem . on ( filter , ( delegator , validator , amount , event ) => {
refreshUserBalance ( userAddress );
showNotification (
`Your unbonding of ${ ethers . formatEther ( amount ) } tokens completed!`
);
});
Filter by validator
// subscribeByValidator.ts
import { stableSystem } from "./config" ;
const validatorAddress = "0x1234..." ;
const validatorFilter = stableSystem . filters . UnbondingCompleted (
null ,
validatorAddress
);
stableSystem . on ( validatorFilter , ( delegator , validator , amount ) => {
updateValidatorStats ( validator , amount );
});
Historical query
If your dApp needs to show a history of past unbonding completions, query historical events using event filters with block ranges.
// queryHistory.ts
import { ethers } from "ethers" ;
import { provider , stableSystem } from "./config" ;
async function getUnbondingHistory (
userAddress : string ,
fromBlock : number ,
toBlock : number
) {
const filter = stableSystem . filters . UnbondingCompleted ( userAddress );
const events = await stableSystem . queryFilter ( filter , fromBlock , toBlock );
return events . map (( event ) => ({
delegator: event . args . delegator ,
validator: event . args . validator ,
amount: ethers . formatEther ( event . args . amount ),
blockNumber: event . blockNumber ,
txHash: event . transactionHash ,
}));
}
const currentBlock = await provider . getBlockNumber ();
const history = await getUnbondingHistory (
"0xabcd..." ,
currentBlock - 1000 ,
currentBlock
);
Step 3: Handle connection issues
Event subscriptions rely on persistent WebSocket connections. Implement reconnection logic for production dApps.
// subscribeWithReconnection.ts
import { ethers } from "ethers" ;
import { STABLE_SYSTEM_ADDRESS , STABLE_SYSTEM_ABI } from "./config" ;
let reconnectAttempts = 0 ;
const MAX_RECONNECT_ATTEMPTS = 5 ;
function handleUnbonding ( delegator : string , validator : string , amount : bigint ) {
console . log ( "Unbonding completed:" , { delegator , validator , amount });
}
function setupEventListener () {
const wsProvider = new ethers . WebSocketProvider ( "wss://rpc.testnet.stable.xyz" );
wsProvider . on ( "error" , ( error ) => {
console . error ( "Provider error:" , error );
if ( reconnectAttempts < MAX_RECONNECT_ATTEMPTS ) {
reconnectAttempts ++;
setTimeout (() => setupEventListener (), 5000 );
}
});
const stableSystem = new ethers . Contract (
STABLE_SYSTEM_ADDRESS ,
STABLE_SYSTEM_ABI ,
wsProvider
);
stableSystem . on ( "UnbondingCompleted" , handleUnbonding );
}
setupEventListener ();
Next recommended
System transactions concept Understand how protocol-level events reach the EVM.
Staking module concept Review the delegation and unbonding flow.
Staking precompile reference Look up the methods that trigger the events tracked here.
Last modified on April 23, 2026