Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5e7378e
feat(bridge): add UTXO reservation core
mswilkison Aug 7, 2026
e9a6370
feat(vault): add ReservationVault
mswilkison Aug 7, 2026
9f55b97
test: cover reservation lifecycle entry points
mswilkison Aug 7, 2026
279fd37
fix(ci): register the Reservation library in deploy and test link maps
mswilkison Aug 7, 2026
bb8dc71
feat(vault): single-mint credit processing and a Bank-balance retry path
mswilkison Aug 7, 2026
37cbfa5
feat(bridge): reserved redemption watchtower veto
mswilkison Aug 7, 2026
9b60336
feat(governance): governance-delayed reservation parameters update
mswilkison Aug 7, 2026
d39a413
test: Bitcoin SPV fixture coverage for reservation proofs
mswilkison Aug 7, 2026
06eff3c
fix(ci): satisfy prettier in the RebateRecovery library map
mswilkison Aug 7, 2026
bc367b0
feat(watchtower): guardian objection flow for reserved redemptions
mswilkison Aug 7, 2026
01a9aa8
feat(validator): reservation proposal validation
mswilkison Aug 7, 2026
5811a8f
test: watchtower veto e2e and proposal validator coverage
mswilkison Aug 7, 2026
d2fde11
fix(ci): hoist guardian check out of the loop condition
mswilkison Aug 7, 2026
a991488
feat(vault): make redemption free by default
mswilkison Aug 7, 2026
fdf7c7b
feat(vault): settle the fee schedule at 40/20/20 with endpoint parity
mswilkison Aug 7, 2026
cc43ec1
fix(reservation): guard redemption-range underflow and floor re-anchors
mswilkison Aug 8, 2026
656d323
docs(config): record measured ~0 runtime-gas cost of the Bridge runs=…
mswilkison Aug 8, 2026
9120328
fix(reservation): relax re-anchor floor to a dust floor (H-08)
mswilkison Aug 8, 2026
5478695
fix(reservation): unblock core deployment workflows
mswilkison Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions solidity/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import "./BridgeState.sol";
import "./Deposit.sol";
import "./DepositSweep.sol";
import "./Redemption.sol";
import "./Reservation.sol";
import "./BitcoinTx.sol";
import "./EcdsaLib.sol";
import "./Wallets.sol";
Expand Down Expand Up @@ -67,6 +68,7 @@ contract Bridge is
using Deposit for BridgeState.Storage;
using DepositSweep for BridgeState.Storage;
using Redemption for BridgeState.Storage;
using Reservation for BridgeState.Storage;
using MovingFunds for BridgeState.Storage;
using Wallets for BridgeState.Storage;
using Fraud for BridgeState.Storage;
Expand Down Expand Up @@ -106,6 +108,64 @@ contract Bridge is
bytes redeemerOutputScript
);

event ReservationAccepted(
uint256 indexed reservationKey,
bytes20 indexed walletPubKeyHash,
address indexed owner,
bytes32 anchorTxHash,
uint64 anchorAmount,
uint32 expiresAt
);

event ReservationExtended(
uint256 indexed reservationKey,
uint32 newExpiresAt
);

event ReservedRedemptionRequested(
uint256 indexed reservationKey,
address indexed redeemer,
bytes redeemerOutputScript,
uint64 mintedAmount,
uint64 txMaxFee
);

event ReservedRedemptionCompleted(
uint256 indexed reservationKey,
bytes32 redemptionTxHash
);

event ReservedRedemptionTimedOut(
uint256 indexed reservationKey,
bytes20 indexed walletPubKeyHash
);

event ReservedRedemptionVetoed(uint256 indexed reservationKey);

event ReservationReanchored(
uint256 indexed reservationKey,
bytes20 indexed newWalletPubKeyHash,
bytes32 newAnchorTxHash,
uint64 newAnchorAmount
);

event ReservationDissolved(
uint256 indexed reservationKey,
bytes20 indexed walletPubKeyHash,
bytes32 dissolutionTxHash
);

event ReservationParametersUpdated(
uint64 reservationMinAmount,
uint64 reservationTxMaxFee,
uint32 reservationTermSeconds,
uint32 reservationGracePeriod,
uint64 reservationMaxTotalAmount,
uint32 maxReservationsPerWallet
);

event ReservationVaultUpdated(address reservationVault);

event WalletMovingFunds(
bytes32 indexed ecdsaWalletID,
bytes20 indexed walletPubKeyHash
Expand Down Expand Up @@ -766,6 +826,99 @@ contract Bridge is
);
}

/// @notice Single entry point for all reservation lifecycle SPV proofs:
/// anchor acceptance, in-kind reserved redemption, re-anchoring
/// and dissolution. Consolidated into one external function to
/// preserve the Bridge's EIP-170 deployment size margin. See
/// `Reservation.submitReservationProof` and the individual
/// handlers in the `Reservation` library for detailed
/// requirements.
/// @param proofType The type of the submitted proof, see
/// `Reservation.ProofType`.
/// @param txInfo Bitcoin transaction data.
/// @param proof Bitcoin proof data.
/// @param mainUtxo Data of the wallet's main UTXO; only used for
/// `Dissolution` proofs and ignored otherwise.
/// @param reservationKey The key of the target reservation; ignored for
/// `Acceptance` proofs where the key is derived from the spent
/// deposit outpoint.
function submitReservationProof(
uint8 proofType,
BitcoinTx.Info calldata txInfo,
BitcoinTx.Proof calldata proof,
BitcoinTx.UTXO calldata mainUtxo,
uint256 reservationKey
) external onlySpvMaintainer {
self.submitReservationProof(
proofType,
txInfo,
proof,
mainUtxo,
reservationKey
);
}

/// @notice Extends the custody term of a reservation by the current
/// reservation term length. Can only be called by the
/// reservation vault, which collects the custody fee for the
/// extension. See `Reservation.extendReservation`.
/// @param reservationKey The key of the reservation to extend.
function extendReservation(uint256 reservationKey) external {
// The caller is checked in the library function.
self.extendReservation(reservationKey);
}

/// @notice Requests an in-kind redemption of a reservation: the wallet
/// is expected to spend exactly the reservation's current anchor
/// outpoint to the redeemer output script in a 1-input-1-output
/// transaction. Can only be called by the reservation vault,
/// which must have approved the Bridge in the Bank for the gross
/// minted amount. See `Reservation.requestReservedRedemption`.
/// @param reservationKey The key of the reservation to redeem.
/// @param redeemer The address able to claim the surrendered balance
/// back if the redemption times out.
/// @param redeemerOutputScript The redeemer's length-prefixed output
/// script (P2PKH, P2WPKH, P2SH or P2WSH).
function requestReservedRedemption(
uint256 reservationKey,
address redeemer,
bytes calldata redeemerOutputScript
) external {
// The caller is checked in the library function.
self.requestReservedRedemption(
reservationKey,
redeemer,
redeemerOutputScript
);
}

/// @notice Notifies that a pending reserved redemption has timed out.
/// Returns the surrendered balance to the redeemer, slashes the
/// wallet operators like a regular redemption timeout, and
/// returns the reservation to the Active state. See
/// `Reservation.notifyReservedRedemptionTimeout`.
/// @param reservationKey The key of the reservation with the timed out
/// redemption.
/// @param walletMembersIDs Identifiers of the wallet signing group
/// members.
function notifyReservedRedemptionTimeout(
uint256 reservationKey,
uint32[] calldata walletMembersIDs
) external {
self.notifyReservedRedemptionTimeout(reservationKey, walletMembersIDs);
}

/// @notice Notifies that a pending reserved redemption was vetoed in the
/// redemption watchtower. Detains the surrendered balance to the
/// watchtower and returns the reservation to the Active state.
/// See `Reservation.notifyReservedRedemptionVeto`.
/// @param reservationKey The key of the reservation with the vetoed
/// redemption.
function notifyReservedRedemptionVeto(uint256 reservationKey) external {
// The caller is checked in the library function.
self.notifyReservedRedemptionVeto(reservationKey);
}

/// @notice Submits the moving funds target wallets commitment.
/// Once all requirements are met, that function registers the
/// target wallets commitment and opens the way for moving funds
Expand Down Expand Up @@ -2080,4 +2233,85 @@ contract Bridge is
// The caller is checked in the internal function.
self.notifyRedemptionVeto(walletPubKeyHash, redeemerOutputScript);
}

/// @notice Updates parameters of reservations, including the
/// reservation vault address. Deposits revealed with the
/// reservation vault address are treated as UTXO reservations.
/// @param reservationVault Address of the reservation vault. Can only be
/// changed while there are no active reservations.
/// @param reservationMinAmount New value of the reservation minimum
/// amount in satoshis. It is the minimal anchor output amount
/// accepted for a reservation.
/// @param reservationTxMaxFee New value of the reservation transaction
/// max fee in satoshis. It is the maximum amount of BTC
/// transaction fee that can be incurred by a single reservation
/// lifecycle transaction.
/// @param reservationTermSeconds New value of the reservation custody
/// term length in seconds.
/// @param reservationGracePeriod New value of the reservation grace
/// period in seconds.
/// @param reservationMaxTotalAmount New cap on the total amount in
/// satoshi locked under active reservations.
/// @param maxReservationsPerWallet New cap on the number of active
/// reservations a single wallet can custody.
/// @dev Requirements:
/// - The caller must be the governance,
/// - See `Reservation.updateReservationParameters` for parameter
/// requirements.
function updateReservationParameters(
address reservationVault,
uint64 reservationMinAmount,
uint64 reservationTxMaxFee,
uint32 reservationTermSeconds,
uint32 reservationGracePeriod,
uint64 reservationMaxTotalAmount,
uint32 maxReservationsPerWallet
) external onlyGovernance {
self.updateReservationParameters(
reservationVault,
reservationMinAmount,
reservationTxMaxFee,
reservationTermSeconds,
reservationGracePeriod,
reservationMaxTotalAmount,
maxReservationsPerWallet
);
}

/// @notice Collection of all reservations indexed by the deposit key of
/// the underlying reserved deposit, i.e.
/// `keccak256(fundingTxHash | fundingOutputIndex)`.
/// @param reservationKey The key of the reservation.
function reservations(uint256 reservationKey)
external
view
returns (Reservation.ReservationRequest memory)
{
return self.reservations[reservationKey];
}

/// @notice Returns the current values of Bridge reservation parameters.
function reservationParameters()
external
view
returns (
address reservationVault,
uint64 reservationMinAmount,
uint64 reservationTxMaxFee,
uint32 reservationTermSeconds,
uint32 reservationGracePeriod,
uint64 reservationMaxTotalAmount,
uint64 reservationTotalAmount,
uint32 maxReservationsPerWallet
)
{
reservationVault = self.reservationVault;
reservationMinAmount = self.reservationMinAmount;
reservationTxMaxFee = self.reservationTxMaxFee;
reservationTermSeconds = self.reservationTermSeconds;
reservationGracePeriod = self.reservationGracePeriod;
reservationMaxTotalAmount = self.reservationMaxTotalAmount;
reservationTotalAmount = self.reservationTotalAmount;
maxReservationsPerWallet = self.maxReservationsPerWallet;
}
}
56 changes: 56 additions & 0 deletions solidity/contracts/bridge/BridgeGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ contract BridgeGovernance is Ownable {
using BridgeGovernanceParameters for BridgeGovernanceParameters.WalletData;
using BridgeGovernanceParameters for BridgeGovernanceParameters.FraudData;
using BridgeGovernanceParameters for BridgeGovernanceParameters.TreasuryData;
using BridgeGovernanceParameters for BridgeGovernanceParameters.ReservationData;

BridgeGovernanceParameters.DepositData internal depositData;
BridgeGovernanceParameters.RedemptionData internal redemptionData;
BridgeGovernanceParameters.MovingFundsData internal movingFundsData;
BridgeGovernanceParameters.WalletData internal walletData;
BridgeGovernanceParameters.FraudData internal fraudData;
BridgeGovernanceParameters.TreasuryData internal treasuryData;
BridgeGovernanceParameters.ReservationData internal reservationData;

Bridge internal bridge;

Expand Down Expand Up @@ -288,6 +290,17 @@ contract BridgeGovernance is Ownable {
event TreasuryUpdateStarted(address newTreasury, uint256 timestamp);
event TreasuryUpdated(address treasury);

event ReservationParametersUpdateStarted(
address newReservationVault,
uint64 newReservationMinAmount,
uint64 newReservationTxMaxFee,
uint32 newReservationTermSeconds,
uint32 newReservationGracePeriod,
uint64 newReservationMaxTotalAmount,
uint32 newMaxReservationsPerWallet,
uint256 timestamp
);

constructor(Bridge _bridge, uint256 _governanceDelay) {
bridge = _bridge;
governanceDelays[0] = _governanceDelay;
Expand Down Expand Up @@ -1807,4 +1820,47 @@ contract BridgeGovernance is Ownable {
function setRebateStaking(address rebateStaking) external onlyOwner {
bridge.setRebateStaking(rebateStaking);
}

/// @notice Begins the reservation parameters update process. All
/// reservation parameters (including the reservation vault) are
/// staged together since they are applied atomically via a
/// single Bridge call.
/// @dev Can be called only by the contract owner.
function beginReservationParametersUpdate(
address _newReservationVault,
uint64 _newReservationMinAmount,
uint64 _newReservationTxMaxFee,
uint32 _newReservationTermSeconds,
uint32 _newReservationGracePeriod,
uint64 _newReservationMaxTotalAmount,
uint32 _newMaxReservationsPerWallet
) external onlyOwner {
reservationData.beginReservationParametersUpdate(
_newReservationVault,
_newReservationMinAmount,
_newReservationTxMaxFee,
_newReservationTermSeconds,
_newReservationGracePeriod,
_newReservationMaxTotalAmount,
_newMaxReservationsPerWallet
);
}

/// @notice Finalizes the reservation parameters update process.
/// @dev Can be called only by the contract owner, after the governance
/// delay elapses.
function finalizeReservationParametersUpdate() external onlyOwner {
BridgeGovernanceParameters.ReservationData
memory staged = reservationData;
reservationData.finalizeReservationParametersUpdate(governanceDelay());
bridge.updateReservationParameters(
staged.newReservationVault,
staged.newReservationMinAmount,
staged.newReservationTxMaxFee,
staged.newReservationTermSeconds,
staged.newReservationGracePeriod,
staged.newReservationMaxTotalAmount,
staged.newMaxReservationsPerWallet
);
}
}
Loading
Loading