This repository contains the specifications and Solidity code for the Union Bridge Contracts.
This project is a work in progress and should be considered experimental. It may contain bugs, security vulnerabilities, and incomplete features.
Use it at your own risk. The author(s) make no guarantees of functionality, stability, or security.
Do not use this software in production environments or for handling sensitive data.
Contributions, feedback, and issue reports are welcome while development is ongoing.
- How it Works
- Peg-In Process (Bitcoin → RSK)
- Peg-Out Process (RSK → Bitcoin)
- Fee Mechanism
- Smart Contracts Architecture
- Bitcoin Transactions
- Musig2
- Security
- Troubleshooting
- You'll need the Rust compiler and Cargo, Rust's package manager. The easiest way to install both is by using rustup.rs.
- Foundry v1.3.1 running
foundryup -i v1.3.1It's important that is this version, otherwise the Alloy version released for the rust crate can change. - Node.js LTS (24)
- Run
forge installto install smart contract dependencies - Run
npm install -g @openzeppelin/upgrades-core@1.44.0to install open zepelin upgrade validations dependencies
We are following Foundry introduction and here are the sections of: best practices - writing contracts best practices - writing tests best practices - writing scripts best practices - security best practices - key management best practices - commenting
We use solidity NatSpec format in all interfaces, libraries, structs, events, errors, and both external and public, functions and variables.
We use a soldity interface called IBridge.sol to interact with the pre compiled contract, this information was obtained from the FastBtc bridge contracts. Since the pow peg bridge is not available locally, we use BridgeMock.sol
The RbtcBridge is a critical intermediary contract that enables RBTC minting and burning operations through the RSK PowPeg Bridge, implementing RSKIP-502.
Why RbtcBridge is needed:
RSKIP-502 requires that the PowPeg Bridge authorize only ONE contract address for minting and burning RBTC. Since the Union Bridge architecture splits responsibilities between PeginManager (handles peg-ins) and PegoutManager (handles peg-outs), we need a single intermediary contract that both managers can use.
Architecture:
┌─────────────────┐
│ PowPeg Bridge │ (authorizes only ONE address)
└───────┬─────────┘
│ authorizes
▼
┌─────────────────┐
│ RbtcBridge │ (single authorized intermediary)
│ - mintRbtc() │ ← Called by PeginManager during pegin acceptance
│ - burnRbtc() │ ← Called by PegoutManager during pegout request
└────────┬────────┘
│ authorized callers
┌────┴────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PeginManager │ │PegoutManager │
└──────────────┘ └──────────────┘
Key Features:
- Access Control: Only the address authorized by AccessManager (PeginManager for mint, PegoutManager for burn) can call
mintRbtc()/burnRbtc() - Gas Limit Protection: RBTC transfers use a 100k gas limit to prevent DoS attacks
- Upgradeable: Uses UUPS pattern for future improvements
- Reentrancy Protection: Implements OpenZeppelin's
nonReentrantmodifier
See RbtcBridge.sol for implementation details.
You can run unit test with:
bash test.shYou can run the local integration test suit with:
bash run.shShow coverage report and create lcov file
bash coverage.shAlso you can check the contract size using:
bash shell/size-report.shAlso, you can check the gas used by the contracts:
bash shell/gas-report.shAlso, you can check the gas usage by the main functions based on the committee size:
bash shell/gas-consumption.shGas consumption needs to be under 80% of max block size (max block size 6.8M gas)
Once we are code ready for a realease, we will run the following command:
bash release.shThis will auto generate the docs and bindings for the rust crate Then we commit this changes and after they are merge we tag the release on github.
Use deployment script to deploy:
# Deploy to testnet
bash shell/script/deploy/deploy-testnet.sh
# Deploy to mainnet
bash shell/script/deploy/deploy-mainnet.sh
# Deploy to alphanet
bash shell/script/deploy/deploy-alphanet.sh
# Deploy to local network (anvil)
bash shell/script/deploy/deploy-local.sh
# Deploy to regtest
bash shell/script/deploy/deploy-regtest.shIt will ask for a private key interactively in order to perform the deployment. The address associated with that private key must have sufficient funds to complete the deployment.
After deploying the contracts to testnet or alphanet, you must configure the PowPeg Bridge to authorize the RbtcBridge contract for RBTC minting and burning operations.
- Deployed contracts (see Deployment section above)
- RbtcBridge contract address from deployment
- Private key with authorization to configure the PowPeg Bridge
castCLI tool from Foundry
The RbtcBridge must be registered as the single authorized union bridge contract:
# Replace placeholders with actual values:
# - <BRIDGE_ADDRESS>: RSK PowPeg Bridge address (0x0000000000000000000000000000000001000006)
# - <RBTC_BRIDGE_ADDRESS>: Your deployed RbtcBridge contract address
# - <RPC_URL>: Your RSK testnet/alphanet/mainnet RPC endpoint
# - <PRIVATE_KEY>: Private key with authorization rights
cast send 0x0000000000000000000000000000000001000006 \
"setUnionBridgeContractAddressForTestnet(address)" \
<RBTC_BRIDGE_ADDRESS> \
--rpc-url <RPC_URL> \
--private-key <PRIVATE_KEY> \
--legacyCheck the current locking cap:
cast call 0x0000000000000000000000000000000001000006 \
"getUnionBridgeLockingCap()" \
--rpc-url <RPC_URL>
# Expected output on alphanet: 400000000000000000000 (400 RBTC in wei)Note: Alphanet already has an initial locking cap of 400 RBTC configured. If you need a higher cap, use increaseUnionBridgeLockingCap() (see "Increasing the Locking Cap" section below).
Verify that the RbtcBridge was registered successfully:
# Verify RbtcBridge is registered as union bridge contract
cast call 0x0000000000000000000000000000000001000006 \
"getUnionBridgeContractAddress()" \
--rpc-url <RPC_URL>
# Expected output: Your RbtcBridge address (should match deployed address)As bridge usage grows, you may need to increase the locking cap:
# RSKIP-502 allows cap increases up to 2x the current total cap
# Current total cap = current lockingCap + already minted amount
cast send 0x0000000000000000000000000000000001000006 \
"increaseUnionBridgeLockingCap(uint256)" \
<NEW_CAP_IN_WEI> \
--rpc-url <RPC_URL> \
--private-key <PRIVATE_KEY> \
--legacyImportant: RSKIP-502 enforces that cap increases:
- Must be greater than the current total cap (available + locked)
- Cannot exceed 2x the current total cap (security measure)
For local testing with Anvil or Regtest, the deployment script automatically:
- Configures BridgeMock with RbtcBridge as the authorized union bridge
- Funds BridgeMock with 400 RBTC for testing
- Sets appropriate confirmations for fast testing
- Note: BridgeMock has a default
lockingCap = 400 etherhardcoded, so Step 2 above is not needed locally
No manual configuration is required for local development. See script/deploy/01_DeployImplAndProxy.s.sol for implementation details.
Running any of the shell/script/deploy/deploy-<testnet/mainnet>.sh scripts will automatically attempt to verify all the contracts to the rsk explorer and rsk blockscout explorer for the selected network. You can also verify contracts directly as shown below.
- Deployed contracts (see Deployment section above)
- Broadcast file from deployment (located in
broadcast/DeployScript.s.sol/<CHAIN_ID>/run-latest.json) - Environment variables configured in
.env:BLOCKSCOUT_MAINNET_API/BLOCKSCOUT_TESTNET_APIRSK_EXPLORER_MAINNET_API/RSK_EXPLORER_TESTNET_API/RSK_EXPLORER_ALPHANET_APIRSK_EXPLORER_MAINNET_URL/RSK_EXPLORER_TESTNET_URL/RSK_EXPLORER_ALPHANET_URL
The easiest way to verify contracts is to use the network-specific verification scripts. You must specify which verifier to use:
# Verify on Blockscout (testnet)
bash shell/script/deploy/verification/verify-testnet.sh blockscout
# Verify on RSK Explorer (testnet)
bash shell/script/deploy/verification/verify-testnet.sh rsk-explorer
# Verify on RSK Explorer (alphanet - Blockscout not available)
bash shell/script/deploy/verification/verify-alphanet.sh rsk-explorer
# Verify on Blockscout (mainnet)
bash shell/script/deploy/verification/verify-mainnet.sh blockscout
# Verify on RSK Explorer (mainnet)
bash shell/script/deploy/verification/verify-mainnet.sh rsk-explorer
# With custom broadcast file
bash shell/script/deploy/verification/verify-testnet.sh blockscout broadcast/DeployScript.s.sol/31/run-latest.jsonAvailable networks:
verify-testnet.sh- Verify contracts on testnetverify-alphanet.sh- Verify contracts on alphanet (RSK Explorer only, no Blockscout)verify-mainnet.sh- Verify contracts on mainnet
Verifier options (required):
blockscout- Verify on Blockscoutrsk-explorer- Verify on RSK Explorer
Note: To verify on both Blockscout and RSK Explorer, run the script twice with different verifier parameters.
These scripts will:
- Verify all implementation contracts (MemberRegistry, CommitteeRegistry, etc.)
- Verify all proxy contracts (ERC1967Proxy instances)
- Extract and format constructor arguments automatically
Note: The verification scripts automatically source .env and determine the appropriate API URLs based on the network. They can be run independently without needing to run the deploy scripts first.
If you need to verify a single contract manually (e.g., if one contract failed during batch verification), you can use the verification functions directly.
For implementation contracts (non-proxy contracts):
# 1. Source .env from project root to get API URLs
source .env
# 2. Change to verification directory (to avoid directory path issues from subsequent scripts)
cd shell/script/deploy/verification
# 3. Source the verification functions
source verify-functions.sh
# 4. Verify a single implementation contract
# Parameters: contract_name, contract_addr, chain_id, verifier, verifier_url
verify_implementation \
"StreamManager" \
"0x0b75fc65eda9ded22f774f3c7045b52024959eb3" \
"31" \
"blockscout" \
"$BLOCKSCOUT_TESTNET_API" local contract_name="$1"
local contract_addr="$2"
local chain_id="$3"
local verifier="$4"
local verifier_url="$5"
verify_implementation \
"StreamManager" \
"0x0b75fc65eda9ded22f774f3c7045b52024959eb3" \
"31" \
"custom" \
"$RSK_EXPLORER_TESTNET_API"Note: Ensure the contract name matches the contract address, otherwise it will not work. Same for the verify_proxy function.
For RSK Explorer, use "custom" as the verifier:
verify_implementation \
"MemberRegistry" \
"0x7CE9FE52C2Dc2bdCD894310D0625187e707e1516" \
"31" \
"custom" \
"$RSK_EXPLORER_TESTNET_API"Note: If anything is changed in the verification scripts, you need to cd back to the verification directory and run source verify-functions.sh again for the changes to take effect.
For proxy contracts (ERC1967Proxy), the function automatically extracts the implementation address and initialization data from the broadcast file:
# 1. Source .env from project root to get API URLs
source .env
# 2. Change to verification directory
cd shell/script/deploy/verification
# 3. Source the verification functions
source verify-functions.sh
# 4. Verify a single proxy contract (uses default broadcast file: broadcast/DeployScript.s.sol/<chain_id>/run-latest.json)
# Parameters: contract_name, proxy_addr, chain_id, verifier, verifier_url, [broadcast_file]
verify_proxy \
"StreamManager" \
"0xcd01fb1cd725e792af40b589796367157bfafe28" \
"31" \
"blockscout" \
"$BLOCKSCOUT_TESTNET_API"For RSK Explorer:
verify_proxy \
"MemberRegistry" \
"0x7a2d268cb4502ed00f01a0f061c507da70fbf25e" \
"31" \
"custom" \
"$RSK_EXPLORER_TESTNET_API"With a custom broadcast file (optional, last parameter):
verify_proxy \
"MemberRegistry" \
"0x7a2d268cb4502ed00f01a0f061c507da70fbf25e" \
"31" \
"blockscout" \
"$BLOCKSCOUT_TESTNET_API" \
"../../../broadcast/DeployScript.s.sol/31/custom-run.json"Note: When using a custom broadcast file, use a relative path from the verification directory, or an absolute path.
Note: The function automatically extracts the implementation address and initialization data from the broadcast file based on the proxy address. If no broadcast file is provided, it defaults to broadcast/DeployScript.s.sol/<chain_id>/run-latest.json. The initialization data is automatically processed to extract the actual initialize() call data by skipping the first 32 bytes (selector + padding).
Note: While ownership can be transferred to any address, it is strongly recommended to use a multisig wallet (such as a Safe multisig) for enhanced security and decentralized control.
Ownership transfer uses a two-step process (Ownable2StepUpgradeable):
- Current owner calls
transferOwnership(newOwner)on each contract. This sets thependingOwnerto the new owner address. - New owner calls
acceptOwnership()on each contract. After this, the new owner becomes theownerandpendingOwneris cleared.
Prerequisites:
- The
<NETWORK>_PEGIN_MANAGERenvironment variable must be set in your.envfile (e.g.,TESTNET_PEGIN_MANAGER,MAINNET_PEGIN_MANAGER,ALPHANET_PEGIN_MANAGER,LOCAL_PEGIN_MANAGER, orREGTEST_PEGIN_MANAGERdepending on the network you're targeting). This address is required as the entry point for the script to discover all other bridge contract addresses on-chain.
Using shell scripts (recommended):
# Transfer ownership (automates step 1 for all contracts)
# Note: The new owner address must start with 0x prefix
bash shell/script/owner/transfer-ownership.sh <network> <new_owner_address>
# Check ownership status
bash shell/script/owner/check-ownership-status.sh <network>Supported networks: testnet, mainnet, alphanet, local, regtest
Which contracts are transferred:
- AccessManager, PeginManager, PegoutManager, OperatorTakeManager, StreamManager, CommitteeRegistry, MemberRegistry, BitcoinManager, SignatureManager, RbtcBridge, ChallengeManager
After running the transfer script:
- Step 1 is complete: Ownership transfers are initiated (the new owner is set as
pendingOwneron all contracts) - Step 2 required: The new owner must call
acceptOwnership()on each contract to complete the transfer- If using a Safe multisig: Use the Safe UI (https://safe.rootstock.io/) to create and execute
acceptOwnership()transactions - If using a regular EOA: Call
acceptOwnership()from that address
- If using a Safe multisig: Use the Safe UI (https://safe.rootstock.io/) to create and execute
To generate the new bindings for the smart contracts run :
bash bind.shIt will automatically generate the rust files for the smart contracts using Alloy
There are some configurations needed to use the pow-peg bridge for burning/minting like locking cap (default 400 RBTC), transfer permissions (default true), and union bridge address (alphanet and testnet only). This can be achived using the methods introduced at RSKIP-502 with an authorizer key.
For local network this is not necesary as it will use the BridgeMock
Example to register the bridge address in alphanet and testnet with setUnionBridgeContractAddressForTestnet
cast send 0x0000000000000000000000000000000001000006 "setUnionBridgeContractAddressForTestnet(address)" 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 --rpc-url http://node-use1-1.alphanet.rskcomputing.net:4444 --legacy --value 0 --gas-limit 500100 --gas-price 4325612 --private-key <RAW_PRIVATE_KEY>To generate the documentation using forge doc:
bash shell/generate-doc.shTo view the generated documentation run:
bash doc.shThe Union Bridge system uses a trust minimized committee approach to manage Bitcoin peg-in and peg-out operations. The process involves members applying to streams, forming committees, and creating packets for processing transactions.
-
Stream: A stream in the Union Bridge is a logical channel that defines parameters such as denomination and operational rules for peg-in and peg-out flows. Streams allow the bridge to support multiple independent flows of assets, each with its own configuration. Committees, composed of operators, and watchtowers, are assigned to each packet within a stream. There can be at most one pegout in process per stream at any time; a new pegout request reverts until the current one is completed or the slot is blocked.
-
Packet: A packet represents a discrete operational period or batch within a stream, during which a specific committee is responsible for processing peg-in and peg-out requests. Each packet contains up to 100 slots. When slot usage reaches the threshold (e.g. 80%), the trigger for creating the next packet and committee happens at request pegin: the first time a request pegin is processed that reserves a slot at or past the threshold, the system triggers committee creation for the next packet. The packet is only created once a new committee is formed to manage it. When all slots in a packet are either BLOCKED or COMPLETED, the packet is closed and the committee is released. Committee members secure the packet by depositing security bonds.
-
Slot: A slot represents a single peg-in or peg-out operation. It's a storage unit within a packet that holds a specific Bitcoin UTXO (Unspent Transaction Output) resulting from a successful peg-in. Slots are created on demand, and when first created, they enter the Prepared state, indicating that all dispute resolution information is in place and the slot is ready to be assigned to a peg-in request. These slots are later used to fulfill peg-out requests, ensuring that the Bitcoin funds are properly accounted for and can be transferred back to users during peg-out operations.
A slot can have the following states:
Prepared, when all the dispute resolution information is linked to the slot (setup completed). In this state the slot is ready to be assigned to a request peg-in operation.Blocked, when pegin is not accepted by committee (Reject Pegin TX) or time window for the committee to sign has passed and the user recovers the funds (User Reimbursement TX). The slot becomes blocked and cannot be used. If that slot is the last one in the packet, the committee is released.Filled, when the Committee members have confirmed and registered a peg-in. In this state the slot is ready for peg-out.Locked, when the slot is assigned to a peg-out operation.OP Selected, when the time window for all members to sign the pegout (User Take TX) has passed and not all members signed. An operator is selected to send the funds to the user.Advanced: when the selected operator advanced funds. If no funds advanced we go back to select an operatorKickoff: when the selected operator presents the reimbursement kickoff tx. If the reimbursement kickoff tx is not presented after some time we go back to select an operator.Challenged: when a member (operator or watchtower) does not think reimbursement is valid it sends a Challenge Tx. After this point the selected operator can't receive the reimbursement (Operator Take TX), and needs to win the challenge.Revealed: when the selected operator reveals the inputs (Reveal Slot ID TX) to be presented to BitVMX dispute. If it does not present the reveal tx we go back to select an operator.Completed: when the peg-out is processed (happy path) or the operator presents the reimbursement after advance the funds without challenge, or operator receives the funds after winning the challenge. If that slot is the last one in the packet, the committee is released.
The StreamManager contract requires specific configuration parameters during initialization. These parameters are defined in the StreamManagerSettingsConfig library (script/helpers/StreamManagerSettingsConfig.sol) and are used by the deployment script (script/deploy/01_DeployImplAndProxy.s.sol) to initialize the StreamManager contract.
The StreamManagerSettings struct configures global settings for the StreamManager:
securityBondPercentageOperator(uint16): Security bond percentage for operators in 10_000 format (e.g., 1000 = 10%). Default: 1000 (10%) for mainnet, 800 (8%) for testnet.securityBondPercentageWatchtower(uint16): Security bond percentage for watchtowers in 10_000 format (e.g., 200 = 2%). Default: 200 (2%) for mainnet, 100 (1%) for testnet.minimumSecurityDeposit(uint256): Minimum security deposit required for committee members in wei. Default: 22,500,000 gwei (2,250 USD).disablementPaymentsPerChallenge(uint256): Amount of disablement payments per challenge in wei. Default: 2,500,000 gwei (250 USD).
Each stream requires a StreamSettings struct that defines:
-
denomination(uint64): The Bitcoin denomination in satoshis. Five streams are created by default:- 0.001 BTC: 100,000 satoshis
- 0.01 BTC: 1,000,000 satoshis
- 0.1 BTC: 10,000,000 satoshis
- 1 BTC: 100,000,000 satoshis
- 10 BTC: 1,000,000,000 satoshis
-
peginConfirmations(uint8): Number of Bitcoin confirmations required for peg-in transactions (request pegin and accept pegin). Default: 12 blocks for mainnet, 1 block for testnet, 2 blocks for local development. -
rejectPeginConfirmations(uint8): Number of Bitcoin confirmations required for reject pegin and user reimbursement transactions. Shorter thanpeginConfirmationsto reduce the DDoS window (e.g. 6 blocks for mainnet). Default: 6 for mainnet, 1 for testnet, 1 for local development. -
pegoutConfirmations(uint8): Number of Bitcoin confirmations required for peg-out transactions. Default: 12 blocks for mainnet, 1 block for testnet, 2 blocks for local development. -
timelockSettings(TimelockSettings): Bitcoin timelock settings in blocks for dispute resolution and protocol operations.
The TimelockSettings struct configures various timelocks used in the Union protocol:
shortTimelock(uint8): Short timelock in Bitcoin blocks. Default: 6 blocks.longTimelock(uint8): Long timelock in Bitcoin blocks. Default: 12 blocks.requestPeginTimelock(uint8): Request peg-in timelock in Bitcoin blocks. Used for user fund recovery if the request peg-in timelock expires. Default: 12 blocks (1 block for local test groups).opWonTimelock(uint8): Operator won timelock in Bitcoin blocks. After this time, if no one challenges the operator, the operator wins the challenge. Default: 150 blocks.claimGateTimelock(uint8): Claim gate timelock in Bitcoin blocks when claim gate is triggered. Default: 6 blocks.inputNotRevealedTimelock(uint8): Input not revealed timelock in Bitcoin blocks. Time the operator has to reveal the input in the challenge; if not, the watchtower wins. Default: 8 blocks.opNoCosignTimelock(uint8): Operator no cosign timelock in Bitcoin blocks when operator does not cosign. Default: 12 blocks.wtNoChallengeTimelock(uint8): Watchtower no challenge timelock in Bitcoin blocks. Time the watchtower has to choose interval for the operator inputs (Challenger Tx); if not, the watchtower is punished. Default: 12 blocks.
These configuration values are used with set var commands in BitVMX to initialize the Union protocol. The values defined in StreamManagerSettingsConfig.sol should match the environment variables set in BitVMX to ensure consistent behavior between the smart contract deployment and the BitVMX client initialization.
The deployment script (01_DeployImplAndProxy.s.sol) automatically retrieves the appropriate settings based on the chain ID and test context, ensuring that the StreamManager is initialized with the correct parameters for each environment.
The packet creation process follows four main phases:
- Member applies to stream: The member's address must be whitelisted (via CommitteeRegistry whitelist APIs). Then the member calls
applyToStream()with their role (Operator/Watchtower) and public keys - Validation: CommitteeRegistry validates public keys and signatures
- Registration: Member is registered and added as a candidate for their requested role
- Committee creation trigger (on apply): When enough members apply (at least
minCommitteeOperatorsandminCommitteeWatchtowers, and at leastcommitteeMemberCounttotal; e.g. 3 operators, 3 watchtowers, 10 total) ANDshouldCreateCommitteefor the stream is true AND there is no pending committee or the pending committee has expired, a pending committee is created. Committee creation can also be triggered at request pegin when slot usage reaches the threshold (see Peg-In Phase 1).
sequenceDiagram
participant M as Member
participant CR as CommitteeRegistry
participant MR as MemberRegistry
participant ENV as Environment
Note over M,ENV: Phase 1: Member Application
Note over M,ENV: Member applies to stream with role (Operator/Watchtower)
M->>+CR: applyToStream(stream, OPERATOR, publicKeys, fundingUTXO)
Note right of M: Sends bond amount + public keys + funding UTXO
CR->>+MR: applyToStream(memberAddress, stream, OPERATOR, publicKeys, fundingUTXO)
Note right of CR: Delegates to MemberRegistry
MR->>MR: _validatePublicKeys()
MR->>MR: _getOrRegisterMember()
MR->>MR: _registerCandidateToStream()
MR-->>-CR: NewSecurityBondDeposit event
CR->>CR: _createCommitteeAfterApplyToStream()
CR-->>-M: NewSecurityBondDeposit event
Note over M,ENV: Additional members apply similarly...
Note over M,ENV: Committee creation triggered when minimum requirements met AND no pending committee exists or the pending committee has expired
-
Automatic creation: When the committee creation trigger is met (i.e.
shouldCreateCommitteefor the stream is true, and there is no pending committee or the pending committee has expired), the system automatically tries to create a committee.shouldCreateCommitteeis set to true when the stream is first created or when the current packet slot usage hits the threshold (e.g. 80%). Slot-usage-based trigger: when a request pegin is processed and the reserved slot reaches the slot-usage threshold, the system callscreateCommitteefor that stream so the next packet and committee are created in time. -
Member selection: Uses Fisher-Yates shuffle to randomly select operators and watchtowers from candidates
-
Committee composition: Ensures at least
committeeMemberCountmembers have applied (e.g. 10), including at leastminCommitteeOperatorsoperators (e.g. 3) and at leastminCommitteeWatchtowerswatchtowers (e.g. 3) -
Pending committee creation: Creates a pending committee with selected members and sets missingData counter
sequenceDiagram
participant CR as CommitteeRegistry
participant MR as MemberRegistry
participant ENV as Environment
Note over CR,ENV: Phase 2: Committee Creation
Note over CR,ENV: Trigger when committeeMemberCount (e.g. 10) min operators (e.g. 3) and min watchtowers (e.g. 3) applied and shouldCreateCommittee true and no pending or expired
Note over CR,ENV: System creates committee by selecting members
CR->>CR: _createCommittee(streamId)
CR->>+MR: selectCommittee(streamId, minWatchtowers, minOperators, committeeMemberCount)
Note right of CR: Delegates member selection to MemberRegistry
MR->>MR: _selectCommittee()
Note right of MR: Check minCommitteeOperators and minCommitteeWatchtowers (e.g. 3 each)
MR->>MR: Randomly select operators from candidates
Note right of MR: Use Fisher-Yates shuffle for selection
MR->>MR: Randomly select watchtowers from candidates
Note right of MR: Select committeeMemberCount members (e.g. 10)
MR-->>-CR: (CommitteeMember[], PendingCommitteeStatus)
CR->>CR: Create pending committee with selected members
CR->>CR: Set missingData counter to member count
CR-->>ENV: NewPendingCommittee event
Note right of CR: Pending committee ready for key deposits
- Deposit communication data: Each selected member in the pending committee deposits their communication data with call depositCommunicationData().
- Deposit aggregated key: Each selected member in the pending committee deposits their aggregated key with call depositAggregatedKey().
- Key validation: All members must provide the same aggregated key
- Committee completion: When all selected members have deposited their keys (missingData reaches 0), the committee is ready
sequenceDiagram
participant M as Member
participant CR as CommitteeRegistry
participant ENV as Environment
Note over M,ENV: Phase 3: Committee Formation
Note over M,ENV: Selected members deposit communication data and then aggregated keys for pending committee
M->>+CR: depositCommunicationData(committeeId, communicationData)
Note right of M: Provides encrypted IP/Port data
CR->>CR: Validate member is in pending committee
CR->>CR: Store communication data
CR->>CR: Decrement missingCommunicationData counter
CR-->>-M: MemberCommunicationDataDeposited event
M->>+CR: depositAggregatedKeys(committeeId, aggregatedTakeKey, aggregatedDisputeKey)
Note right of M: Provides aggregated public key (33 bytes)
CR->>CR: Validate member is in pending committee
CR->>CR: Store aggregated key
CR->>CR: Decrement missingData counter
CR-->>-M: MemberInfoDeposited event
Note over M,ENV: All selected committee members deposit their communication data and keys...
Note over M,ENV: When missingData reaches 0, committee is complete!
- Committee registration: A unique committee ID is generated and the committee is registered
- Balance updates: Pre-staked amounts are moved to staked amounts for all committee members
- Packet creation: StreamManager creates a new packet with the committee
- Cleanup: Pending committee data is cleaned up
- Committee ready: The committee is now active and ready to handle peg-in and peg-out operations
sequenceDiagram
participant CR as CommitteeRegistry
participant MR as MemberRegistry
participant SM as StreamManager
participant ENV as Environment
Note over CR,ENV: Phase 4: Committee Registration & Packet Creation
Note over CR,ENV: Committee is registered and new packet is created
CR->>CR: Generate committeeId (hash of streamId + packetNumber)
CR->>+MR: stakePreStakedCandidatesBalance(committeeMembers, streamDenomination, packetNumber)
Note right of CR: Delegates balance updates to MemberRegistry
MR->>MR: Move pre-staked to staked for all members
MR->>MR: Remove members from candidates pool
MR-->>-CR: Balance updates completed
CR->>CR: _registerCommittee()
CR-->>ENV: NewCommittee event
CR->>+SM: createNewPacket(streamId, committeeId, aggregatedTakePubKey, disputeKeys)
SM->>SM: Create new packet with committee
SM-->>-CR: Packet created
CR->>CR: _deletePendingCommittee()
Note right of CR: Clean up pending committee data
Note over CR,ENV: Committee Ready for Operations
Note over CR,ENV: Committee is now active and ready for peg operations
- User generates temporary address: User calls
getRequestPeginData()to get a Bitcoin committee address for deposit - User deposits BTC: User sends Bitcoin to the generated temporary address, including an OP_RETURN output with the RSK address where they want to receive the funds. For detailed information about the REQUEST_PEGIN_TX transaction structure, inputs/outputs, and Taproot script details.
- Member submits request: A committee member who monitors the Bitcoin network calls
requestPegin()with the Bitcoin transaction and SPV proof - System validates: System validates the transaction, reserves a slot, and stores the request. If the reserved slot reaches the slot-usage threshold (e.g. 80%), the system triggers committee creation for the next packet so a new packet will be ready when needed.
- Generate accept transaction: System generates the Bitcoin accept peg-in transaction and emits an event with the signature hash for committee members to sign
sequenceDiagram
participant U as User
participant M as Member
participant PIM as PeginManager
participant ENV as Environment
Note over U,ENV: Phase 1: Request Peg-In
Note over U,ENV: User requests to peg-in Bitcoin to RSK
U->>+PIM: getRequestPeginData(rootstockAddress, value, btcReimbursementPubKey)
PIM-->>-U: temporaryPeginAddress
U->>U: Send BTC to temporaryPeginAddress
Note right of U: User deposits Bitcoin to the generated address
M->>+PIM: requestPegin(btcTxSPVProof)
Note right of M: Committee member monitors Bitcoin network and submits transaction
PIM->>PIM: Validate BTC transaction and SPV proof
PIM->>PIM: Reserve slot and store request pegin data
Note right of PIM: If slot usage reaches threshold then createCommittee(streamId)
PIM->>PIM: Generate accept peg-in transaction
PIM-->>-M: PeginRequested event
Note right of PIM: Event includes signature hash for committee members
- Operators register take and won tx hashes: Committee members with the operator role call
addOperatorTakeTxids()to register the operator take and won transactions hashes before signatures are collected. For detailed information see the OPERATOR_TAKE_TX and OPERATOR_WON_TX transaction structure, inputs/outputs, and spending conditions. - Create accept pegin transaction: System creates the ACCEPT_PEGIN_TX transaction that will spend the REQUEST_PEGIN_TX output. For detailed information about the ACCEPT_PEGIN_TX transaction structure, inputs/outputs, and committee signature requirements.
- Committee members sign: Each committee member signs the accept peg-in transaction using
addMemberNonce()andaddMemberSignature()from SignatureManager - Signature collection: Signatures are collected and validated by the SignatureManager
- Ready for broadcast: Once all committee members have signed, the signed transaction is ready to be broadcast to the Bitcoin network
sequenceDiagram
participant O as Operator
participant M as Member
participant SM as SignatureManager
participant ENV as Environment
Note over O,ENV: Phase 2: Committee Signatures for Peg-In
Note over O,ENV: Operators register the operator take and won transaction hashes before signatures
loop For each operator
O->>+SM: addOperatorTakeTxids(acceptPeginTxid, operatorTakeTxid, operatorWonTxid)
Note right of O: Operator registers the operator take and won transaction hashes
SM-->>-ENV: OperatorTakeTxidsAdded event
end
SM-->>ENV: AllOperatorTakeTxidsAdded event
Note right of SM: Event emitted when all operators have registered their hashes
Note over M,ENV: Committee members sign the accept peg-in transaction
loop For each committee member
M->>+SM: addMemberNonce(txid, nonce)
Note right of M: Member provides their nonce
SM->>SM: Store member nonce
SM-->>-M: Nonce stored
M->>+SM: addMemberSignature(txid, signature)
Note right of M: Member signs the accept peg-in transaction
SM->>SM: Validate and store signature
SM-->>-ENV: SignatureAdded event
end
SM-->>ENV: AllSignaturesReady event
Note right of SM: Event emitted when all members have signed
Note right of SM: Signed transaction is ready for broadcast to Bitcoin network
- Member submits accept: A committee member who monitors the Bitcoin network calls
acceptPegin()with the broadcasted transaction and SPV proof - System validates: System validates the accept transaction and proof
- Store UTXO in slot: The accept peg-in UTXO is stored in a slot for future use in peg-out operations
- Mint RBTC: PeginManager calls
RbtcBridge.mintRbtc()which requests RBTC from the PowPeg Bridge and sends it to the user's RSK address
sequenceDiagram
participant M as Member
participant PIM as PeginManager
participant ENV as Environment
Note over M,ENV: Phase 3: Accept Peg-In
Note over M,ENV: Member submits the broadcasted accept peg-in transaction
M->>+PIM: acceptPegin(btcTxSPVProof)
Note right of M: Committee member monitors Bitcoin network and submits broadcasted transaction
PIM->>PIM: Validate BTC transaction and SPV proof
PIM->>PIM: Validate committee signatures
PIM->>PIM: Process pegin acceptance
PIM->>PIM: Mint RBTC to user via RbtcBridge
PIM-->>-ENV: PeginAccepted event
Note right of PIM: BTC is now pegged-in to RSK, RBTC minted to user's address
-
User Reimbursement: After a time window the user can spend the request pegin transaction to recover the funds. If this is the case someone who monitors the Bitcoin network calls
userReimbursement()with the broadcasted USER_REIMBUSEMENT_TX and SPV proof to mark that slot as BLOCKED. If that slot is the last one of the packet, the committee is released. -
Reject Pegin: For some reason the committee does not accept the request pegin. A member broadcasts a REJECT_PEGIN_TX to consume the enabler and calls
rejectPegin()with the SPV proof of that transaction to mark the slot as BLOCKED. If that slot is the last one of the packet, the committee is released.
sequenceDiagram
participant M as Monitor/Member
participant PIM as PeginManager
participant SM as StreamManager
participant CR as CommitteeRegistry
participant ENV as Environment
Note over M,ENV: Reject Pegin or User Reimbursement. Slot BLOCKED. If last slot in packet then committee released.
alt User Reimbursement
M->>+PIM: userReimbursement(btcTxSPVProof)
else Reject Pegin
M->>+PIM: rejectPegin(btcTxSPVProof)
end
PIM->>+SM: blockSlot(acceptPeginTxid)
SM->>SM: Set slot state to BLOCKED
SM->>SM: Mark slot as finished
SM-->>-PIM: packetClosed
alt Last slot of packet
PIM->>+CR: releaseCommittee(streamId, packetNumber)
CR-->>-ENV: Committee released
end
PIM-->>-ENV: RejectPeginRegistered or UserReimbursementRegistered event
At most one pegout can be in process per stream at any time. A new pegout request (tryPegout) reverts with PegoutInProcess until the current pegout is completed (e.g. via registerUserTake, registerOperatorTake, or registerOperatorWon) or the slot is blocked (reject pegin or user reimbursement).
- User requests pegout: User calls
tryPegout()with the Bitcoin public key where they want to receive funds and sends RBTC equal to the stream denomination - Validate request: System validates the Bitcoin compressed public key format and amount limits
- Lock slot: System finds a filled slot and marks it as locked. Reverts if no filled slot is available or if there is a pegout in process
- Store request: Peg-out request is stored with all the necessary data
- Generate user take transaction: System generates the Bitcoin user take transaction using the stored UTXO from the previous accept peg-in and emits an event with the signature hash for committee members to sign
- Burn RBTC: PegoutManager calls
RbtcBridge.burnRbtc()which releases RBTC back to the PowPeg Bridge, preparing for the Bitcoin peg-out
sequenceDiagram
participant U as User
participant POM as PegoutManager
participant ENV as Environment
Note over U,ENV: Phase 1: Peg-Out Request
Note over U,ENV: User requests to peg-out RBTC to Bitcoin
U->>+POM: tryPegout(userPubKey)
Note right of U: Provides Bitcoin public key and sends RBTC
POM->>POM: Validate request
POM->>POM: Lock slot
Note right of POM: Find filled slot and mark as locked<br/>Reverts if no filled slot available<br/>or pegout in process
POM->>POM: Store pegout request data
POM->>POM: Generate user take transaction
POM->>POM: Burn RBTC via RbtcBridge
POM-->>-ENV: PegoutRequested event
Note right of POM: Event includes signature hash for committee members
Note right of POM: RBTC burned and returned to PowPeg Bridge via RbtcBridge
- Committee members sign: Each committee member signs the user take pegout transaction and registers their signature with the SignatureManager using
addMemberNonce()andaddMemberSignature() - Signature validation: System tracks when all signatures are collected
- Emit completion event: System emits an event when all committee members have signed
sequenceDiagram
participant M as Member
participant SM as SignatureManager
participant ENV as Environment
Note over M,ENV: Phase 2: Committee Signatures for Peg-Out
Note over M,ENV: Committee members sign the user take pegout
loop For each committee member
M->>+SM: addMemberNonce(txid, nonce)
Note right of M: Member provides their nonce
SM->>SM: Store member nonce
SM-->>-M: Nonce stored
M->>+SM: addMemberSignature(txid, signature)
Note right of M: Member signs user take pegout transaction
SM->>SM: Validate and store signature
SM-->>-ENV: SignatureAdded event
end
SM->>SM: Signature validation - The system controls that all signatures are collected.
SM-->>ENV: AllSignaturesReady event
Note right of SM: Event emitted when all members have signed
- Execute pegout: Member executes the Bitcoin transaction sending BTC to the user's Bitcoin address when all signatures are collected. For detailed information about the USER_TAKE_TX transaction structure, inputs/outputs, and spending conditions.
- Submit BTC transaction: Member calls
registerUserTake()with the Bitcoin transaction and SPV proof - Validate transaction: System validates the BTC transaction and proof
- Validate signatures: Committee signatures are validated
- Peg-out Registered: System emits an event PegoutRegistered informing that RBTC is now linked to Bitcoin. If the completed slot was the last one in the packet, the committee is released.
sequenceDiagram
participant M as Member
participant POM as PegoutManager
participant ENV as Environment
Note over M,ENV: Normal Case: UserTake (Take0) - All Members Signed
Note over M,ENV: Member executes pegout and registers it
M->>M: Bitcoin pegout transaction
Note right of M: BTC sent to user's Bitcoin address when all signatures collected
M->>+POM: registerUserTake(btcTxSPVProof)
Note right of M: Member calls `registerUserTake()` with the Bitcoin transaction and SPV proof
POM->>POM: Validate BTC transaction and SPV proof
POM->>POM: Validate committee signatures
POM-->>-ENV: PegoutRegistered event
Note right of POM: RBTC is now pegged-out to Bitcoin
If not all committee members sign within the timeout period:
- Trigger operator take: A member calls
OperatorTakeManager.triggerOperatorTake()to start the operator take process, which emits an event indicating which operator needs to do the funds advancement. A unique PEGOUT ID is created at this step (derived from stream position, operator take public key, current bitcoin block hash, version of the pegout id and an incrementing sequence number). This pegout ID is included in theOperatorTakeTriggeredevent and must be embedded in the ADVANCE_FUNDS_TX OP_RETURN output for later verification. - Cancel user take flow: Selected operator cancels the user take flow before advancing the funds.
- Operator advances funds: An operator advances BTC to the user's Bitcoin address. For detailed information about the ADVANCE_FUNDS_TX transaction structure, inputs/outputs, and spending conditions.
- Broadcast Reimbursement Kickoff: The operator broadcasts a Reimbursement Kickoff Bitcoin transaction. When the operator calls
OperatorTakeManager.registerReimbursementKickoff()with the SPV proof, the contract sets the BASE EVENT on the RBTC bridge (viaRbtcBridge.setBaseEvent) to the 32-byte pegout ID. This base event is used by the bridge for tracking and must be set before the operator take flow can complete. - Challenge period: If no one challenges within the timeout period, the member proceeds
- Broadcast Operator Take transaction: The operator broadcasts the Operator Take (Take1) Bitcoin transaction. For detailed information about the OPERATOR_TAKE_TX transaction structure, inputs/outputs, and spending conditions.
- Submit BTC transaction: Operator calls
OperatorTakeManager.registerOperatorTake()with the Bitcoin transaction and SPV proof - Validate transaction: System validates the BTC transaction and proof
- Peg-out Registered: System emits an event PegoutRegistered informing that RBTC is now linked to Bitcoin via operator take. If the completed slot was the last one in the packet, the committee is released.
If the selected operator fails to complete their required steps within the takeTimeout.operatorTake period, any member can call OperatorTakeManager.triggerOperatorTake() again to select a new operator:
- From OP_SELECTED: Operator failed to register
OperatorTakeManager.registerAdvanceFunds()within the timeout. - From ADVANCED: Operator registered advance funds but failed to register
OperatorTakeManager.registerReimbursementKickoff()within the timeout. Status resets toOP_SELECTED. - From KICKOFF: Operator registered kickoff but failed to register
OperatorTakeManager.registerOperatorTake()within the timeout. Status resets toOP_SELECTED.
The timeout window resets each time the operator makes progress (each call to registerAdvanceFunds() or registerReimbursementKickoff() updates the operatorTakeUpdatedAt timestamp).
If the operator's REIMBURSEMENT_KICKOFF_TX is challenged by a watchtower:
- Challenge registered: A watchtower calls
registerChallenge()after detecting incorrect behavior (e.g., invalid ADVANCE_FUNDS_TX). For detailed information about the CHALLENGE_TX transaction structure, inputs/outputs, and spending conditions. - Operator reveals input: The operator must respond by broadcasting REVEAL_INPUT_TX to prove they advanced funds correctly. The operator signs the slot ID using their Winternitz SLOT_ID_KEY. For detailed information about the REVEAL_INPUT_TX transaction structure, inputs/outputs, and spending conditions.
- Automatic dispatch: The Dispute Core protocol automatically dispatches OPERATOR_WON_TX after REVEAL_INPUT_TX is confirmed, scheduled for execution after OP_WON_TIMELOCK blocks expire (default: 150 blocks).
- Broadcast Operator Won transaction: After the timelock expires, the operator broadcasts the Operator Won (Take2) Bitcoin transaction. For detailed information about the OPERATOR_WON_TX transaction structure, inputs/outputs, and spending conditions.
- Submit BTC transaction: Operator calls
OperatorTakeManager.registerOperatorWon()with the SPV proof of OPERATOR_WON_TX transaction. - Validate transaction: System validates the BTC transaction and proof
- Peg-out Registered: System emits an event PegoutRegistered informing that RBTC is now linked to Bitcoin via operator won (disputed fallback)
sequenceDiagram
participant M as Member
participant OTM as OperatorTakeManager
participant RB as RbtcBridge
participant ENV as Environment
Note over M,ENV: Alternative: Operator Take (Take1) - not all members signed
Note over M,ENV: When not all members sign within timeout
M->>+OTM: triggerOperatorTake(acceptPeginTxid)
Note right of M: Member triggers operator take after timeout.<br/>Pegout ID created and emitted in OperatorTakeTriggered
OTM->>OTM: Validate timeout and signatures status
OTM-->>-ENV: OperatorTakeTriggered event (includes pegoutId)
M->>M: Bitcoin user funds advancement
Note right of M: Operator advances BTC to user's Bitcoin address
M->>+OTM: registerAdvanceFunds(acceptPeginTxid, btcTxSPVProof)
Note right of M: Operator calls `registerAdvanceFunds()` with the advancement tx and SPV proof
OTM->>OTM: Validate BTC transaction and SPV proof
OTM-->>-ENV: AdvanceFundsRegistered event
M->>M: Bitcoin Reimbursement Kickoff
Note right of M: Operator broadcasts Reimbursement Kickoff Bitcoin transaction
M->>+OTM: registerReimbursementKickoff(acceptPeginTxid, btcTxSPVProof)
Note right of M: Operator calls `registerReimbursementKickoff()` with the kickoff tx and SPV proof
OTM->>OTM: Validate BTC transaction and SPV proof
OTM->>RB: setBaseEvent(pegoutId)
OTM-->>-ENV: ReimbursementKickoffRegistered event
Note over M,ENV: Challenge period timeout
Note over M,ENV: If no one challenges within timeout period
M->>M: Bitcoin Operator Take (Take1)
Note right of M: Operator broadcasts the final Operator Take Bitcoin transaction
M->>+OTM: registerOperatorTake(btcTxSPVProof)
Note right of M: Operator calls `registerOperatorTake()` with the Bitcoin transaction and SPV proof
OTM->>OTM: Validate BTC transaction and proof
OTM-->>-ENV: PegoutRegistered event
Note right of OTM: RBTC is now pegged-out to Bitcoin via operator take
If the operator's REIMBURSEMENT_KICKOFF_TX is challenged by a watchtower:
- Challenge registered: A watchtower calls
ChallengeManager.registerChallenge()after detecting incorrect behavior (e.g., invalid ADVANCE_FUNDS_TX). For detailed information about the CHALLENGE_TX transaction structure, inputs/outputs, and spending conditions. - Operator reveals input: The operator must respond by broadcasting REVEAL_INPUT_TX to prove they advanced funds correctly. The operator signs the slot ID using their Winternitz SLOT_ID_KEY. A member calls
ChallengeManager.registerInputRevealed()with the SPV proof. For detailed information about the REVEAL_INPUT_TX transaction structure, inputs/outputs, and spending conditions. - Automatic dispatch: The Dispute Core protocol automatically dispatches OPERATOR_WON_TX after REVEAL_INPUT_TX is confirmed, scheduled for execution after OP_WON_TIMELOCK blocks expire (default: 150 blocks).
- Broadcast Operator Won transaction: After the timelock expires, the operator broadcasts the Operator Won (Take2) Bitcoin transaction. For detailed information about the OPERATOR_WON_TX transaction structure, inputs/outputs, and spending conditions.
- Submit BTC transaction: Operator calls
OperatorTakeManager.registerOperatorWon()with the SPV proof of OPERATOR_WON_TX transaction. - Validate transaction: System validates the BTC transaction and proof
- Peg-out Registered: System emits an event PegoutRegistered informing that RBTC is now linked to Bitcoin via operator won (disputed fallback). If the completed slot was the last one in the packet, the committee is released.
sequenceDiagram
participant WT as Watchtower
participant Op as Operator
participant BTC as Bitcoin Blockchain
participant CM as ChallengeManager
participant OTM as OperatorTakeManager
participant ENV as Environment
Note over Op,ENV: Operator Take (Take1) - Challenge Dispute Flow
Note over Op,ENV: When operator is challenged after REIMBURSEMENT_KICKOFF_TX
WT->>BTC: 1. Dispatch CHALLENGE_TX
BTC-->>WT: Transaction mined
WT->>+CM: registerChallenge(acceptPeginTxid, btcTxSPVProof)
Note right of WT: Watchtower challenges via ChallengeManager
CM->>CM: Validate BTC transaction and SPV proof
CM-->>-ENV: ChallengeRegistered event
Note right of CM: Status: CHALLENGE
Op->>BTC: 2. Dispatch REVEAL_INPUT_TX
Note right of Op: Operator reveals slot ID signature<br/>proving correct fund advancement
BTC-->>Op: Transaction mined
Op->>+CM: registerInputRevealed(acceptPeginTxid, btcTxSPVProof)
Note right of Op: Member/operator registers reveal on-chain
CM->>CM: Validate REVEAL_INPUT_TX and SPV proof
CM-->>-ENV: InputRevealed event
Note right of Op: Dispute Core automatically schedules<br/>OPERATOR_WON_TX after OP_WON_TIMELOCK
Note over Op,BTC: Wait for OP_WON_TIMELOCK blocks (150 blocks)
Op->>BTC: 3. Dispatch OPERATOR_WON_TX
BTC-->>Op: Transaction mined
Op->>+OTM: registerOperatorWon(btcTxSPVProof)
Note right of Op: Operator calls `OperatorTakeManager.registerOperatorWon()`<br/>with OPERATOR_WON_TX and SPV proof
OTM->>OTM: Validate BTC transaction and SPV proof
OTM-->>-ENV: PegoutRegistered event
Note right of OTM: Reimbursement to the operator who advanced<br/>funds (user was already pegged-out in advance funds)
The Union Bridge employs a fee mechanism to cover Bitcoin network transaction costs. Here's how fees work in the current implementation:
- User sends BTC: User sends the full stream denomination (e.g., 100,000 satoshis) to the temporary pegin address
- BTC transaction fees deducted: The accept pegin transaction deducts fees for Bitcoin network operations:
P2TR_FEE: 335 satoshis (Taproot transaction fee)SPEED_UP_AMOUNT: 540 satoshis (fee for speed-up output)- Total deducted: 875 satoshis
- RBTC minted: User receives RBTC equivalent to
acceptPeginAmount = denomination - fees- Example: 100,000 - 875 = 99,125 satoshis worth of RBTC
- User sends RBTC: User must send the full stream denomination in RBTC (e.g., 100,000 satoshis worth)
- RBTC burned: Contract burns only what was originally minted during pegin:
- Burned amount:
acceptPeginAmount(e.g., 99,125 satoshis worth) - See
PegoutManager.sol:110
- Burned amount:
- Fee accumulation: The difference between what the user sent and what was burned remains in the contract:
- Accumulated fees:
msg.value - acceptPeginAmount(e.g., 875 satoshis worth of RBTC) - These fees accumulate in
PegoutManager
- Accumulated fees:
Users effectively pay BTC transaction fees twice:
- Once in BTC during the pegin (deducted from their Bitcoin deposit)
- Once in RBTC during the pegout (paid as part of the full denomination requirement)
This ensures the bridge system has funds to cover Bitcoin network fees for both pegin and pegout operations.
Currently, accumulated fees remain in the PegoutManager contract and can only be withdrawn by the contract owner.
Future implementation: Fees will be distributed to operators and watchtowers as compensation for their services in running the bridge, including:
- Operating committee members who sign transactions
- Watchtowers who monitor for disputes
- Operators who advance funds in timeout scenarios
Fee amounts are defined in src/libraries/Constants.sol:
P2TR_FEE = 335satoshisSPEED_UP_AMOUNT = 540satoshis
Note: This is the initial implementation. Fee stru cture and distribution mechanisms may be refined in future versions based on operational requirements and economic analysis.
Using the 0.001 BTC (100,000 satoshis) stream denomination:
Pegin:
- User sends: 100,000 sats in BTC
- Pegin BTC tx fees deducted: 875 sats (335 P2TR + 540 speed-up)
- User receives: 99,125 sats worth of RBTC
Pegout:
- User sends: 100,000 sats worth of RBTC (full denomination required)
- RBTC burned: 99,125 sats worth (only what was minted)
- Fees kept in contract: 875 sats worth of RBTC
- Pegout starts with the already-reduced amount: 99,125 sats BTC UTXO
- Pegout BTC tx fees deducted: ~875 sats (assuming similar network conditions)
- User receives: ~98,250 sats in BTC (99,125 - 875)
Total User Cost:
- Lost to pegin fees: 875 sats (paid in BTC during pegin)
- Lost to pegout RBTC fees: 875 sats (paid in RBTC, kept by contract)
- Lost to pegout BTC network fees: ~875 sats (deducted from the 99,125 sat UTXO)
- Total: ~2,625 satoshis for a complete round-trip (pegin + pegout)
The BitVMX Union Bridge is a sophisticated smart contract system that enables secure Bitcoin peg-in and peg-out operations between Bitcoin and Rootstock (RSK). The system uses a committee-based approach with multiple specialized contracts working together to manage the bridge operations.
graph TB
%% Core Contracts
PIM[PeginManager<br/>Handles Bitcoin → RSK operations]
POM[PegoutManager<br/>Handles RSK → Bitcoin peg-out request]
OTM[OperatorTakeManager<br/>Operator take flow management]
CM[ChallengeManager<br/>Dispute resolution management]
PB[PegBase<br/>Shared abstract base contract]
PMB[PegManagerBase<br/>Shared abstract base contract]
AM[AccessManager<br/>Authorization and pause controller]
BM[BitcoinManager<br/>Bitcoin address generation and validation]
CR[CommitteeRegistry<br/>Committee formation and management]
MR[MemberRegistry<br/>Member registration and balance tracking]
SM[StreamManager<br/>Stream and packet management]
SigM[SignatureManager<br/>Multi-signature operations]
RB[RbtcBridge<br/>RSKIP-502 RBTC mint/burn intermediary]
%% External Dependencies
Bridge[RSK PowPeg Bridge<br/>External Precompiled Contract]
%% Inheritance
PMB -.inherits.-> PB
PIM -.inherits.-> PMB
POM -.inherits.-> PMB
OTM -.inherits.-> PMB
CM -.inherits.-> PB
%% RbtcBridge - RSKIP-502 Single Authorized Address
Bridge -->|authorizes<br/>single address| RB
RB -->|mintRbtc| PIM
RB -->|burnRbtc| POM
%% PeginManager Relationships
PIM --> BM
PIM --> CR
PIM --> SM
PIM --> SigM
PIM -->|calls mintRbtc| RB
%% PegoutManager Relationships
POM --> BM
POM --> CR
POM --> SM
POM --> SigM
POM --> MR
POM -->|calls burnRbtc| RB
%% OperatorTakeManager Relationships
OTM --> BM
OTM --> CR
OTM --> SM
OTM --> SigM
OTM --> POM
OTM -->|setBaseEvent| RB
%% ChallengeManager Relationships
CM --> BM
CM --> CR
CM --> SM
CM --> OTM
%% AccessManager Relationships - Pause Control
AM -.controls pause.-> PIM
AM -.controls pause.-> POM
AM -.controls pause.-> OTM
AM -.controls pause.-> CR
AM -.controls pause.-> MR
AM -.controls pause.-> RB
AM -.controls pause.-> CM
%% AccessManager Relationships - Authorization Checks
SM -.checks authorization.-> AM
RB -.checks authorization.-> AM
CR -.checks authorization.-> AM
SigM -.checks authorization.-> AM
MR -.checks authorization.-> AM
%% Other Relationships
CR --> MR
CR --> SM
CR --> PIM
CR --> POM
CR --> OTM
MR --> SM
MR --> CR
SigM --> CR
SigM --> PIM
SigM --> POM
SigM --> OTM
SM --> PIM
SM --> POM
SM --> OTM
SM --> CR
%% Styling
classDef coreContract fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef baseContract fill:#c5e1a5,stroke:#33691e,stroke-width:2px
classDef accessContract fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
classDef bridgeContract fill:#ffe0b2,stroke:#e65100,stroke-width:3px
classDef external fill:#fff3e0,stroke:#e65100,stroke-width:2px
class PIM,POM,OTM,CM,BM,CR,MR,SM,SigM coreContract
class PB,PMB baseContract
class AM accessContract
class RB bridgeContract
class Bridge external
- Purpose: Manages peg-in operations from Bitcoin to Rootstock
- Key Features:
- Handles Bitcoin peg-in requests with SPV proofs
- Processes peg-in acceptance with committee signatures
- Generates temporary Bitcoin addresses for deposits
- Mints RBTC to users via RbtcBridge after successful peg-in
- Coordinates with StreamManager for slot allocation
- Integrates with CommitteeRegistry for committee management
- Inherits shared functionality from PegManagerBase (which inherits from PegBase)
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Manages peg-out operations from Rootstock to Bitcoin
- Key Features:
- Processes peg-out requests and burns RBTC via RbtcBridge
- Registers user take (Take0) when all members sign; operator take flows are delegated to OperatorTakeManager
- Coordinates committee signatures for peg-outs
- Integrates with MemberRegistry for operator management
- Inherits shared functionality from PegManagerBase (which inherits from PegBase)
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Manages operator take flow when not all committee members sign within the user take timeout
- Key Features:
triggerOperatorTake(acceptPeginTxid)– member triggers operator take; creates pegout ID and selects operatorregisterAdvanceFunds(acceptPeginTxid, btcTxSPVProof)– operator registers ADVANCE_FUNDS_TXregisterReimbursementKickoff(acceptPeginTxid, btcTxSPVProof)– operator registers kickoff; sets RbtcBridge base eventregisterOperatorTake(btcTxSPVProof)– operator registers OPERATOR_TAKE_TX (Take1)registerOperatorWon(btcTxSPVProof)– operator registers OPERATOR_WON_TX (Take2, disputed case)- Manages user take and operator take timeouts; allows re-triggering to select a new operator on timeout
- Coordinates with PegoutManager (pegout context), StreamManager, SignatureManager, CommitteeRegistry, RbtcBridge (setBaseEvent)
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Abstract base contract providing shared functionality for PeginManager, PegoutManager, OperatorTakeManager, and ChallengeManager
- Key Features:
- Centralizes common state variables (bitcoinManager, streamManager, committeeRegistry)
- Provides shared initialization logic with AccessManager integration
- Implements common peg status validation functions
- Inherits from BaseProxy, ReentrancyGuardUpgradeable, and Pausable
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Abstract base contract providing shared functionality for PeginManager, PegoutManager, and OperatorTakeManager
- Key Features:
- Extends PegBase with additional manager-specific functionality
- Centralizes common state variables (signatureManager, rbtcBridge)
- Provides shared initialization logic
- Stream/signature manager and pause authority are configured at initialization or via upgrade (no post-deploy setters in this base)
- Purpose: RSKIP-502 intermediary contract that serves as the single authorized address for RBTC minting and burning with the PowPeg Bridge
- Key Features:
- Acts as the single authorized contract registered with the PowPeg Bridge (RSKIP-502 requirement)
- Provides
mintRbtc()function exclusively for PeginManager to mint RBTC during peg-in acceptance - Provides
burnRbtc()function exclusively for PegoutManager to burn RBTC during peg-out requests - Provides
verifyTxConfirmations()andgetTxBlockNumberAndVerifyConfirmations()functions to verify Bitcoin transaction confirmations using RSK bridge precompiled contract - Provides
getBestBlockHash()function to retrieve the hash of the best Bitcoin block - Implements strict access control via AccessManager: only PeginManager can call mint, only PegoutManager can call burn
- Provides
setBaseEvent(pegoutId)RSKIP-529 base event for bridge tracking - Enforces 100k gas limit on RBTC transfers to prevent DoS attacks
- Handles all PowPeg Bridge error codes (cap exceeded, transfers disabled, unauthorized caller)
- Security Features: UUPS upgradeable, non-reentrant, pausable via AccessManager
- Critical Role: Without RbtcBridge, both managers cannot interact with PowPeg Bridge due to single-address constraint. Additionally, ChallengeManager, PeginManager, PegoutManager, OperatorTakeManager, and MemberRegistry depend on RbtcBridge's transaction verification functions (
verifyTxConfirmations,getTxBlockNumberAndVerifyConfirmations,getBestBlockHash) to validate Bitcoin transactions and block data
- Purpose: Handles Bitcoin address generation and transaction validation
- Key Features:
- Creates temporary Bitcoin addresses for peg-in requests
- Validates Bitcoin transactions and SPV proofs
- Generates signature hashes for Bitcoin transactions
- Manages Taproot addresses with key spend and script spend paths
- Security Features: UUPS upgradeable
- Purpose: Manages committee formation, selection, and lifecycle
- Key Features:
- Creates and manages committees for different Bitcoin denominations
- Handles committee member selection and rotation
- Manages pending committee formation with timeouts
- Coordinates with MemberRegistry for member management
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Manages member registration, applications, and balance tracking
- Key Features:
- Handles member registration with public key validation
- Manages security bond deposits and withdrawals
- Tracks member balances (available, pre-staked, staked)
- Supports member applications to different streams
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Manages streams and packet allocation for different Bitcoin denominations
- Key Features:
- Creates and manages streams for different Bitcoin amounts
- Handles packet creation and slot allocation
- Manages committee assignments to packets
- Tracks stream usage and committee rotation
- Security Features: UUPS upgradeable
- Purpose: Manages multi-signature operations for committee members
- Key Features:
- Handles Musig2 protocol for committee signatures
- Manages signature collection for peg-in/peg-out operations
- Tracks operator take transaction IDs
- Coordinates with CommitteeRegistry for member verification
- Security Features: UUPS upgradeable
- Purpose: Manages challenge operations for dispute resolution in peg-out flows
- Key Features:
- Handles challenge registration when operators present invalid reimbursement transactions
- Manages input revelation for BitVMX dispute resolution
- Validates challenge transactions and SPV proofs
- Coordinates with OperatorTakeManager for challenge context (operator take dispute flow)
- Inherits shared functionality from PegBase
- Security Features: Pausable (via AccessManager), UUPS upgradeable, non-reentrant
- Purpose: Centralized authorization and pause controller for emergency stops
- Key Features:
- Extends PauseManager with role-based access control
- Provides authorization checks for sensitive operations across the bridge system
- Enforces permissions through view functions that restrict sensitive operations (peg status modifications, committee management, packet creation, RBTC operations, signature initialization, member management) to authorized contracts only
- Single control point for pausing all contracts
- Coordinates pause/unpause across PeginManager, PegoutManager, OperatorTakeManager, CommitteeRegistry, MemberRegistry, RbtcBridge, and ChallengeManager
- Owner-controlled with single transaction emergency stop
- Provides system-wide pause status checking
- Security Features: UUPS upgradeable, owner access control
- All contracts use UUPS (Universal Upgradeable Proxy Standard) for upgradeability
- Only the contract owner can authorize upgrades
- Implementation contracts can be upgraded without changing proxy addresses
- PeginManager, PegoutManager, OperatorTakeManager, CommitteeRegistry, MemberRegistry, RbtcBridge, and ChallengeManager are pausable
- AccessManager (which extends PauseManager) provides centralized pause control for all contracts
- Pause functionality allows emergency stops of critical operations with a single transaction
- Only AccessManager owner can pause/unpause the system
- All pausable contracts delegate pause authority to AccessManager
- BaseProxy provides ownership functionality through OpenZeppelin's Ownable2StepUpgradeable
- AccessManager contract provides role-based access control
- PeginManager, PegoutManager, OperatorTakeManager, and ChallengeManager have privileges over other contracts (enforced via AccessManager)
- All contracts implement non-reentrant patterns
- External calls are made after state changes (checks-effects-interactions pattern)
- Reentrancy guards prevent recursive calls
- Multi-signature operations using Musig2 protocol
- Committee rotation and member selection
- Security bonds and staking mechanisms
- Timeout-based committee formation
- PeginManager manages peg-in operations, coordinating with BitcoinManager, CommitteeRegistry, StreamManager, SignatureManager, and RbtcBridge for minting RBTC
- PegoutManager manages peg-out requests and user take (Take0), coordinating with BitcoinManager, CommitteeRegistry, StreamManager, SignatureManager, MemberRegistry, and RbtcBridge for burning RBTC
- OperatorTakeManager manages operator take flow (Take1, Take2), coordinating with PegoutManager, StreamManager, SignatureManager, CommitteeRegistry, RbtcBridge, and BitcoinManager
- ChallengeManager manages challenge operations for dispute resolution, coordinating with OperatorTakeManager, BitcoinManager, CommitteeRegistry, and StreamManager
- RbtcBridge serves as the single authorized intermediary between both managers and the PowPeg Bridge (RSKIP-502), handling all RBTC minting and burning operations
- AccessManager controls pause state and provides role-based access control for PeginManager, PegoutManager, OperatorTakeManager, CommitteeRegistry, MemberRegistry, RbtcBridge, and ChallengeManager
- PegBase provides shared base functionality for PeginManager, PegoutManager, OperatorTakeManager, and ChallengeManager
- PegManagerBase extends PegBase with additional manager-specific functionality for PeginManager, PegoutManager, and OperatorTakeManager
- CommitteeRegistry manages committee lifecycle and coordinates with MemberRegistry and StreamManager
- StreamManager handles stream and packet management, working with CommitteeRegistry
- SignatureManager processes multi-signature operations for committees
- BitcoinManager provides Bitcoin-specific functionality as a utility contract
- MemberRegistry manages member data and balances across all other contracts
The system is deployed using a proxy pattern where:
- Each contract has an implementation contract
- A proxy contract delegates calls to the implementation
- The proxy stores the state while the implementation contains the logic
- Upgrades are performed by changing the implementation address in the proxy
The deployment is created using the deployment scripts and information about deployed contracts can be found in the broadcast folder.
This architecture ensures security, upgradeability, and maintainability while providing a robust foundation for Bitcoin-RSK bridge operations.
MuSig2 allows groups of mutually distrusting parties to cooperatively sign data and aggregate their signatures into a single aggregated signature which is indistinguishable from a signature made by a single private key. The group collectively controls an aggregated public key which can only create signatures if everyone in the group cooperates (AKA an N-of-N multisignature scheme). MuSig2 is optimized to support secure signature aggregation with only two round-trips of network communication.
Specifically we use the smart contracts to verify Musig2 partial signatures, in order to slash dishonest committee participants. We followed rust musgi2 library implementation that uses BIP-0327: MuSig2 for BIP340-compatible Multi-Signatures, for creating and verifying signatures which validate under Bitcoin consensus rules.
If you’re not already familiar with MuSig2, the process of cooperative signing runs like so:
-
All signers share their public keys with one-another. The group computes an
aggregated publickey which they collectively control. -
In the first signing round, signers generate and share nonces (random numbers) with one-another. These nonces have both secret and public versions. Only the public nonce (AKA PubNonce) should be shared, while the corresponding secret nonce (AKA SecNonce) must be kept secret.
-
Once every signer has received the public nonces of every other signer, each signer makes a partial signature for a message using their secret key and secret nonce.
-
In the second signing round, signers share their partial signatures with one-another. Partial signatures can be verified to place blame on misbehaving signers (but are not themselves unforgeable).
-
A valid set of partial signatures can be aggregated into a final signature, which is just a normal Schnorr signature, valid under the aggregated public key.
We are using Slither static analyzer to check for potentials threats. We are running it through the docker image eth-security-toolbox from trail of bits. Using the following command:
docker compose upWe are also using Open Zeppelin foundry upgrades for deploying and managing upgradeable contracts, which includes upgrade safety validations.
If you see something like
[FAIL: revert: Failed to run upgrade safety validation: /Users/pmprete/.npm/_npx/e9c2fe9985ed1095/node_modules/@openzeppelin/upgrades-core/dist/cli/validate/build-info-file.js:127
throw new error_1.ValidateCommandError(`Build info file ${buildInfoFilePath} is not from a full compilation.`, () => PARTIAL_COMPILE_HELP);
^
ValidateCommandError: Build info file out/build-info/001d9012b78cf83be88732141551bdb6.json is not from a full compilation.Then recompile all contracts with the following commands and try again:
forge clean && forge buildIf you still get the error delete the out folder

