Skip to content

Commit

Permalink
Redemptions veto mechanism (#788)
Browse files Browse the repository at this point in the history
Closes: #781

Here we present the implementation of the redemptions veto mechanism,
specified in the [TIP-072: Optimistic
redemptions](https://forum.threshold.network/t/tip-072-optimistic-redemptions/793).

### High-level architecture

The current byte size of the `Bridge` contract (~22 kB) is close to the
limit. To leave some space for future `Bridge` upgrades, we decided to
encapsulate the logic of the redemption veto mechanism in a separate
upgradeable contract called `RedemptionWatchtower`, and attach it to the
existing `Bridge` instance. Such a design has a positive impact on the
`Bridge` size, nicely separates the concerns, and reduces the amount of
direct changes in the living `Bridge` instance.
 
The `RedemptionWatchtower` contract acts as a central point for
redemption guardians, manages vetoed redemptions, and provides other
functionality specified in the TIP. It also serves as a source of
information about vetoed redemptions, banned redeemers, and processing
delays that should be preserved for specific redemption requests.

The chart below presents how the `RedemptionWatchtower` fits into the
existing architecture and provides an overview of interactions between
relevant smart contracts and system actors:

```
                                +---------------+
                                |               |
                      +---------+    Wallets    +-------------+
                      |         |               |             |
   pendingRedemptions |         +---------------+             | validateRedemptionProposal
                      |                                       |
                      |                                       |
                      v                                       v
+--------------------------+                          +-----------------------------+
|                          |     pendingRedemptions   |                             |
|          Bridge          |<-------------------------+   WalletProposalValidator   |
|                          |                          |                             |
+----+---------------------+                          +-------+---------------------+
     |                ^                                       |
     |                |                                       |
     |                |                                       |
     |                |                                       | getRedemptionDelay
     |                | notifyRedemptionVeto                  |
     |                +---------+                             |
     |                          |                             |
     |                          |                             |
     | isSafeRedemption    +----+---------------------+       |
     +-------------------->|                          |<------+
                           |   RedemptionWatchtower   |
                  +------->|                          |<------+
                  |        +--------------------------+       |
                  |                     ^                     |
                  |                     |                     |
                  |                     |                     |
   raiseObjection |                     | management          | security
                  |                     | actions             | actions
                  |                     |                     |
                  |                     |                     |
          +-------+-------+     +-------+-------+     +-------+-------+
          |               |     |               |     |               |
          |   Guardians   |     |    Manager    |     |     Owner     |
          |               |     |               |     |               |
          +---------------+     +---------------+     +---------------+
```

### The `RedemptionWatchtower` contract

The `RedemptionWatchtower` contract interacts with several actors of the
protocol. Each actor has a different set of capabilities:

**Owner (Threshold Council)**

The owner can call the following functions:
- `enableWatchtower` which enables the veto mechanism for the first
time. This function must set the watchtower's manager and can establish
an initial set of guardians. This function also captures the timestamp
of the call. This information is necessary to ensure the proper
lifecycle of the veto mechanism (shut down after 18 months). It is also
crucial for determining stalled redemptions that were created in the
pre-veto era and can be vetoed indefinitely.
- `removeGuardian` that can be used to remove specific guardians. This
function is a safeguard against malicious guardians hence, it must be
directly available to the owner.

**Manager (Token holder DAO)**

As per the TIP, most of the governance should be the authority of the
Token holder DAO. To make it possible, we are introducing the role of
the watchtower's manager who can use the following functions:
- `addGuardian` which allows adding new redemption guardians
- `updateWatchtowerParameters` that can update all governable parameters
that steer the veto mechanism. Those parameters are redemption veto
delays, veto time and financial penalties, and the mechanism lifetime.
- `unban` which can be used to unban redeemers who were banned
mistakenly. This is a safeguard for guardian mistakes that must be in
place to protect honest redeemers.

**Guardians**

Guardians are the executors of the veto process. The only function they
can call is `raiseObjection`. That function should be used to raise
objections against specific redemption requests. Three subsequent
objections lead to a redemption veto. Vetoed redemptions are not
processed by the protocol, the requested amount is diminished by a
penalty fee, and frozen for a specific time. Once the freeze period
ends, the redeemer can claim those funds back. Moreover, redeemers whose
redemptions were vetoed become banned and cannot ask for redemptions in
the future. Last but not least, even a single objection (not leading to
a veto) against a redemption (from a given wallet to a given BTC
address) prevents asking the same wallet to redeem to the same BTC
address in the future (this is similar to timed out redemptions).

**Others**

The `RedemptionWatchtower` exposes some functions available to the broad
public. Those are:
- `isSafeRedemption` which determines whether a redemption involving a
specific wallet, BTC address, Bank's balance owner, and redeemer can be
considered as safe. This function leverages the objections history and
banned redeemers to determine so. A redemption is considered safe if
neither the balance owner nor redeemer is banned and past redemptions
from the given wallet to the given BTC address were not subject to
guardian objections.
- `getRedemptionDelay` that informs tBTC wallets about processing delays
that should be preserved for specific redemption requests. Delays are
determined based on raised objections.
- `withdrawVetoedFunds` which can be used by redeemers to withdraw funds
from vetoed redemptions once the freeze period ends.
- `disableWatchtower` that can be used by anyone to disable the veto
mechanism once the watchtower's lifetime elapses.

### Changes in the `Bridge` contract

Integration of the `RedemptionWatchtower` contract with the existing
`Bridge` incurs several changes in the latter.

First and foremost, the `Bridge`'s state gains a new address field
(`redemptionWatchtower`) that points to the `RedemptionWatchtower`
contract. That field can be set using the new `setRedemptionWatchtower`
function available for the `Bridge`'s governance.

Secondly, the `Bridge` exposes a new `notifyRedemptionVeto` callback
function. This function can be called only by the `redemptionWatchtower`
address. The main responsibility of this function is propagating the
consequences of a redemption veto to the `Bridge`'s state, namely:
- Reduce the pending redemptions value of the target wallet (supposed to
handle the vetoed redemption). This is the same action as performed upon
redemption timeout. This must be done because otherwise, the wallet
would hold the reserve for a redemption request that would never be
processed
- Delete the redemption request from the pending redemptions mapping.
This is important to avoid the vetoed redemption request being processed
by the wallet or reported as timed out
- Detain the redemption's request amount and transfer it under the
control of the redemption watchtower for further processing (i.e. burn
the penalty fee and allow withdrawal of the rest after the freeze
period)

Last but not least, the `Bridge` leverages the `redemptionWatchtower`
address to determine the safety of upcoming redemption requests. This is
achieved using the `isSafeRedemption` call. This way, redemptions from
banned redeemers (and balance owners) are blocked. The same applies to
redemptions from specific wallets to specific BTC addresses which were
subject to guardian objections in the past.

### Changes in the `WalletProposalValidator` contract

The last piece of the puzzle is the integration of the veto mechanism
with the off-chain tBTC wallets. Wallets must respect the new veto delay
and not process redemptions during the period a redemption veto can
still land. According to
[RFC-12](https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-12.adoc#224-introduce-the-walletproposalvalidator-contract),
the tBTC wallets always consult the `WalletProposalValidator` to
validate the ongoing redemption proposal before issuing the redemption
transaction on the Bitcoin chain. That means it is enough to modify the
`WalletProposalValidator.validateRedemptionProposal` function and take
the veto delay into account there. The `validateRedemptionProposal`
function calls the `RedemptionWatchtower.getRedemptionDelay` function
for each redemption in the proposal and considers it valid only if the
returned veto delay (counted since the redemption creation timestamp)
already elapsed. This logic guarantees that a particular redemption
proposal can only be considered valid if all of its redemption requests
have surpassed their respective veto delay periods.

In light of the above, we should also prevent invalid redemption
proposals (with redemptions violating the veto delay) from being issued
by RFC-12 coordinators. An invalid proposal means a coordination window
miss and the optimal strategy here is ensuring proposal validity at
generation time. This requires adjustments in the [off-chain redemption
proposal generator
logic](https://github.com/keep-network/keep-core/blob/2a76f4de820eb13b2d49c67a15a59eac7552fa26/pkg/tbtcpg/redemptions.go#L70).
This work is beyond the scope of this pull request and will be addressed
in a follow-up PR.

The ultimate protection against wallets not obeying the aforementioned
rules is the fact that vetoed redemptions are removed from the
`pendingRedemption` set in the `Bridge`. That means the `Bridge` will
reject an SPV proof of a redemption transaction that handles at least
one vetoed redemption request (that happens in the
`submitRedemptionProof` function). In such a case, the given wallet will
be deemed as fraudulent and punished according to the protocol rules.
  • Loading branch information
tomaszslabon authored Mar 6, 2024
2 parents 9e047d1 + c6c03a3 commit f3f8d62
Show file tree
Hide file tree
Showing 15 changed files with 5,010 additions and 633 deletions.
46 changes: 46 additions & 0 deletions solidity/contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ contract Bridge is

event TreasuryUpdated(address treasury);

event RedemptionWatchtowerSet(address redemptionWatchtower);

modifier onlySpvMaintainer() {
require(
self.isSpvMaintainer[msg.sender],
Expand Down Expand Up @@ -1944,4 +1946,48 @@ contract Bridge is
function txProofDifficultyFactor() external view returns (uint256) {
return self.txProofDifficultyFactor;
}

/// @notice Sets the redemption watchtower address.
/// @param redemptionWatchtower Address of the redemption watchtower.
/// @dev Requirements:
/// - The caller must be the governance,
/// - Redemption watchtower address must not be already set,
/// - Redemption watchtower address must not be 0x0.
function setRedemptionWatchtower(address redemptionWatchtower)
external
onlyGovernance
{
// The internal function is defined in the `BridgeState` library.
self.setRedemptionWatchtower(redemptionWatchtower);
}

/// @return Address of the redemption watchtower.
function getRedemptionWatchtower() external view returns (address) {
return self.redemptionWatchtower;
}

/// @notice Notifies that a redemption request was vetoed in the watchtower.
/// This function is responsible for adjusting the Bridge's state
/// accordingly.
/// The results of calling this function:
/// - the pending redemptions value for the wallet is decreased
/// by the requested amount (minus treasury fee),
/// - the request is removed from pending redemptions mapping,
/// - the tokens taken from the redeemer on redemption request are
/// detained and passed to the redemption watchtower
/// (as Bank's balance) for further processing.
/// @param walletPubKeyHash 20-byte public key hash of the wallet.
/// @param redeemerOutputScript The redeemer's length-prefixed output
/// script (P2PKH, P2WPKH, P2SH or P2WSH).
/// @dev Requirements:
/// - The caller must be the redemption watchtower,
/// - The redemption request identified by `walletPubKeyHash` and
/// `redeemerOutputScript` must exist.
function notifyRedemptionVeto(
bytes20 walletPubKeyHash,
bytes calldata redeemerOutputScript
) external {
// The caller is checked in the internal function.
self.notifyRedemptionVeto(walletPubKeyHash, redeemerOutputScript);
}
}
16 changes: 16 additions & 0 deletions solidity/contracts/bridge/BridgeGovernance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1766,4 +1766,20 @@ contract BridgeGovernance is Ownable {
function governanceDelay() internal view returns (uint256) {
return governanceDelays[0];
}

/// @notice Sets the redemption watchtower address. This function does not
/// have a governance delay as setting the redemption watchtower is
/// a one-off action performed during initialization of the
/// redemption veto mechanism.
/// @param redemptionWatchtower Address of the redemption watchtower.
/// @dev Requirements:
/// - The caller must be the owner,
/// - Redemption watchtower address must not be already set,
/// - Redemption watchtower address must not be 0x0.
function setRedemptionWatchtower(address redemptionWatchtower)
external
onlyOwner
{
bridge.setRedemptionWatchtower(redemptionWatchtower);
}
}
35 changes: 33 additions & 2 deletions solidity/contracts/bridge/BridgeState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ library BridgeState {
// timed out. It is counted from the moment when the redemption
// request was created via `requestRedemption` call. Reported
// timed out requests are cancelled and locked TBTC is returned
// to the redeemer in full amount.
// to the redeemer in full amount. If a redemption watchtower
// is set, the redemption timeout should be greater than the maximum
// value of the redemption delay that can be enforced by the watchtower.
// Consult `IRedemptionWatchtower.getRedemptionDelay` for more details.
uint32 redemptionTimeout;
// The amount of stake slashed from each member of a wallet for a
// redemption timeout.
Expand Down Expand Up @@ -314,14 +317,17 @@ library BridgeState {
// HASH160 over the compressed ECDSA public key) to the basic wallet
// information like state and pending redemptions value.
mapping(bytes20 => Wallets.Wallet) registeredWallets;
// Address of the redemption watchtower. The redemption watchtower
// is responsible for vetoing redemption requests.
address redemptionWatchtower;
// Reserved storage space in case we need to add more variables.
// The convention from OpenZeppelin suggests the storage space should
// add up to 50 slots. Here we want to have more slots as there are
// planned upgrades of the Bridge contract. If more entires are added to
// the struct in the upcoming versions we need to reduce the array size.
// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
// slither-disable-next-line unused-state
uint256[50] __gap;
uint256[49] __gap;
}

event DepositParametersUpdated(
Expand Down Expand Up @@ -374,6 +380,8 @@ library BridgeState {

event TreasuryUpdated(address treasury);

event RedemptionWatchtowerSet(address redemptionWatchtower);

/// @notice Updates parameters of deposits.
/// @param _depositDustThreshold New value of the deposit dust threshold in
/// satoshis. It is the minimal amount that can be requested to
Expand Down Expand Up @@ -826,4 +834,27 @@ library BridgeState {
self.treasury = _treasury;
emit TreasuryUpdated(_treasury);
}

/// @notice Sets the redemption watchtower address.
/// @param _redemptionWatchtower Address of the redemption watchtower.
/// @dev Requirements:
/// - Redemption watchtower address must not be already set,
/// - Redemption watchtower address must not be 0x0.
function setRedemptionWatchtower(
Storage storage self,
address _redemptionWatchtower
) internal {
require(
self.redemptionWatchtower == address(0),
"Redemption watchtower already set"
);

require(
_redemptionWatchtower != address(0),
"Redemption watchtower address must not be 0x0"
);

self.redemptionWatchtower = _redemptionWatchtower;
emit RedemptionWatchtowerSet(_redemptionWatchtower);
}
}
117 changes: 117 additions & 0 deletions solidity/contracts/bridge/Redemption.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,45 @@ import "./Wallets.sol";

import "../bank/Bank.sol";

/// @notice Interface of the RedemptionWatchtower.
interface IRedemptionWatchtower {
/// @notice Determines whether a redemption request is considered safe.
/// @param walletPubKeyHash 20-byte public key hash of the wallet that
/// is meant to handle the redemption request.
/// @param redeemerOutputScript The redeemer's length-prefixed output
/// script (P2PKH, P2WPKH, P2SH or P2WSH) that is meant to
/// receive the redeemed amount.
/// @param balanceOwner The address of the Bank balance owner whose balance
/// is getting redeemed.
/// @param redeemer The address that requested the redemption and will be
/// able to claim Bank balance if anything goes wrong during the
/// redemption. In the most basic case, when someone redeems their
/// Bitcoin balance from the Bank, `balanceOwner` is the same
/// as `redeemer`. However, when a Vault is redeeming part of its
/// balance for some redeemer address (for example, someone who has
/// earlier deposited into that Vault), `balanceOwner` is the Vault,
/// and `redeemer` is the address for which the vault is redeeming
/// its balance to.
/// @return True if the redemption request is safe, false otherwise.
/// Specific safety criteria depend on the implementation.
function isSafeRedemption(
bytes20 walletPubKeyHash,
bytes calldata redeemerOutputScript,
address balanceOwner,
address redeemer
) external view returns (bool);

/// @notice Returns the applicable redemption delay for a redemption
/// request identified by the given redemption key.
/// @param redemptionKey Redemption key built as
/// `keccak256(keccak256(redeemerOutputScript) | walletPubKeyHash)`.
/// @return Redemption delay.
function getRedemptionDelay(uint256 redemptionKey)
external
view
returns (uint32);
}

/// @notice Aggregates functions common to the redemption transaction proof
/// validation and to the moving funds transaction proof validation.
library OutboundTx {
Expand Down Expand Up @@ -382,6 +421,8 @@ library Redemption {
/// `amount - (amount / redemptionTreasuryFeeDivisor) - redemptionTxMaxFee`.
/// Fees values are taken at the moment of request creation.
/// @dev Requirements:
/// - If the redemption watchtower is set, the redemption request must
/// be considered safe by the watchtower,
/// - Wallet behind `walletPubKeyHash` must be live,
/// - `mainUtxo` components must point to the recent main UTXO
/// of the given wallet, as currently known on the Ethereum chain,
Expand All @@ -402,6 +443,19 @@ library Redemption {
bytes memory redeemerOutputScript,
uint64 amount
) internal {
if (self.redemptionWatchtower != address(0)) {
require(
IRedemptionWatchtower(self.redemptionWatchtower)
.isSafeRedemption(
walletPubKeyHash,
redeemerOutputScript,
balanceOwner,
redeemer
),
"Redemption request rejected by the watchtower"
);
}

Wallets.Wallet storage wallet = self.registeredWallets[
walletPubKeyHash
];
Expand Down Expand Up @@ -1075,4 +1129,67 @@ library Redemption {
}
return key;
}

/// @notice Notifies that a redemption request was vetoed in the watchtower.
/// This function is responsible for adjusting the Bridge's state
/// accordingly.
/// The results of calling this function:
/// - the pending redemptions value for the wallet is decreased
/// by the requested amount (minus treasury fee),
/// - the request is removed from pending redemptions mapping,
/// - the tokens taken from the redeemer on redemption request are
/// detained and passed to the redemption watchtower
/// (as Bank's balance) for further processing.
/// @param walletPubKeyHash 20-byte public key hash of the wallet.
/// @param redeemerOutputScript The redeemer's length-prefixed output
/// script (P2PKH, P2WPKH, P2SH or P2WSH).
/// @dev Requirements:
/// - The caller must be the redemption watchtower,
/// - The redemption request identified by `walletPubKeyHash` and
/// `redeemerOutputScript` must exist.
function notifyRedemptionVeto(
BridgeState.Storage storage self,
bytes20 walletPubKeyHash,
bytes calldata redeemerOutputScript
) external {
require(
msg.sender == self.redemptionWatchtower,
"Caller is not the redemption watchtower"
);

uint256 redemptionKey = getRedemptionKey(
walletPubKeyHash,
redeemerOutputScript
);
Redemption.RedemptionRequest storage redemption = self
.pendingRedemptions[redemptionKey];

// Should never happen, but just in case.
require(
redemption.requestedAt != 0,
"Redemption request does not exist"
);

// Update the wallet's pending redemptions value. This is the
// same logic as performed upon redemption request timeout.
// If we don't do this, the wallet will hold the reserve
// for a redemption request that will never be processed.
self.registeredWallets[walletPubKeyHash].pendingRedemptionsValue -=
redemption.requestedAmount -
redemption.treasuryFee;

// Capture the amount that should be transferred to the
// redemption watchtower. Use the whole requested amount as a detained
// amount because the treasury fee is deducted in `submitRedemptionProof`.
// Since the redemption did not happen, the treasury fee was not
// deducted and the whole requested amount should be detained.
uint64 detainedAmount = redemption.requestedAmount;

// Delete the redemption request from the pending redemptions
// mapping. This is important to avoid this redemption request
// to be processed by the wallet or reported as timed out.
delete self.pendingRedemptions[redemptionKey];

self.bank.transferBalance(self.redemptionWatchtower, detainedAmount);
}
}
Loading

0 comments on commit f3f8d62

Please sign in to comment.