Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions solidity/contracts/bridge/IReservationBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ interface IReservationBridge {
bytes20 targetWalletPubKeyHash
) external;

/// @notice See `ReservationRouter.requestReservedRedemption`.
function requestReservedRedemption(
uint256 reservationKey,
address redeemer,
bytes calldata redeemerOutputScript,
bool feePaid,
bool useRetryCredit
) external;

/// @notice See `ReservationRouter.notifyReservationRedemptionTimedOut`.
function notifyReservationRedemptionTimedOut(
uint256 reservationKey,
uint32[] calldata walletMembersIDs
) external;

/// @notice See `ReservationRouter.notifyReservedRedemptionVeto`.
function notifyReservedRedemptionVeto(
uint256 reservationKey,
uint64 requestNonce
) external;

/// @notice See `ReservationRouter.extendReservation`.
function extendReservation(uint256 reservationKey) external;

/// @notice See `ReservationRouter.submitReservationProof`.
function submitReservationProof(
uint8 proofType,
Expand Down
45 changes: 45 additions & 0 deletions solidity/contracts/bridge/Redemption.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,51 @@ interface IRedemptionWatchtower {
external
view
returns (uint32);

/// @notice Returns the applicable veto delay for the given pending
/// reserved redemption generation. This is the delay the
/// Bridge proof path enforces before a reserved redemption
/// generation can settle: wallets must not sign before it
/// elapses.
/// @param reservationKey The key of the reservation.
/// @param requestNonce The reserved redemption generation.
/// @return Reserved redemption veto delay.
function getReservedRedemptionDelay(
uint256 reservationKey,
uint64 requestNonce
) external view returns (uint32);

/// @notice Returns the current three-level veto delay schedule to
/// snapshot for a newly requested reserved redemption.
/// @param requestedAmount Reserved redemption amount in satoshis.
/// @return reservedDefaultDelay Delay before any guardian objection.
/// @return reservedLevelOneDelay Delay after one guardian objection.
/// @return reservedLevelTwoDelay Delay after two guardian objections.
/// @dev Returns an all-zero schedule when no guardian veto policy
/// applies at request time: the watchtower is not enabled, has
/// been disabled, or the requested amount is below the waiver
/// limit.
function getReservedRedemptionDelaySchedule(uint64 requestedAmount)
external
view
returns (
uint32 reservedDefaultDelay,
uint32 reservedLevelOneDelay,
uint32 reservedLevelTwoDelay
);

/// @notice Determines whether a reserved redemption request is
/// considered safe: neither the reservation owner nor the
/// redeemer may be banned. Objection state is per generation,
/// so no historical objection count is consulted -- a fresh
/// generation always starts with a clean count.
/// @param owner The reservation owner.
/// @param redeemer The address requesting the redemption.
/// @return True if the reserved redemption request is safe.
function isSafeReservedRedemption(address owner, address redeemer)
external
view
returns (bool);
}

/// @notice Aggregates functions common to the redemption transaction proof
Expand Down
231 changes: 231 additions & 0 deletions solidity/contracts/bridge/RedemptionWatchtower.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ pragma solidity 0.8.17;
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

import "./Bridge.sol";
import "./IReservationBridge.sol";
import "./Redemption.sol";
import "./Reservation.sol";

/// @title Redemption watchtower
/// @notice This contract encapsulates the logic behind the redemption veto
Expand Down Expand Up @@ -469,6 +471,235 @@ contract RedemptionWatchtower is OwnableUpgradeable {
);
}

/// @notice Computes the veto-proposal key of a reserved redemption
/// generation. The key space is disjoint from the regular
/// redemption key space (`keccak256(keccak256(redeemerOutputScript)
/// | walletPubKeyHash)`): a reservation key and generation nonce
/// cannot collide with a redeemer output script hash.
function reservedVetoKey(uint256 reservationKey, uint64 requestNonce)
public
pure
returns (uint256)
{
return
uint256(keccak256(abi.encodePacked(reservationKey, requestNonce)));
}

/// @notice Raises an objection to a pending reserved redemption
/// generation. Works the same way as `raiseObjection` does for
/// regular redemption requests: each pending reserved
/// redemption has a delay period during which the wallet is not
/// allowed to process it and the guardians can raise objections
/// to; the third objection vetoes it. A veto detains the
/// escrowed gross amount from the Bridge, freezes it for the
/// freeze period, burns the penalty fee, and bans the redeemer.
/// The reservation itself returns to the Active state on the
/// Bridge -- the anchor outpoint was not spent, so the in-kind
/// claim survives (though a banned owner cannot re-request
/// until unbanned). The vetoed generation never accepts an SPV
/// proof.
/// @param reservationKey The key of the reservation with the pending
/// reserved redemption.
/// @param requestNonce The pending redemption generation.
/// @dev Requirements:
/// - The caller must be a redemption guardian,
/// - The generation must not have been vetoed already,
/// - The guardian must not have already objected to it,
/// - The generation must be the reservation's pending redemption,
/// - The generation must be within the veto delay snapshotted for
/// its current objection level; a permanently disabled watchtower
/// makes the effective delay zero.
/// Deliberate divergence from the pooled path: a generation
/// requested before the watchtower was ever enabled snapshots an
/// all-zero delay schedule and can therefore never be objected to
/// (there is no `VetoPeriodCheckOmitted` escape hatch here).
/// Harmless under the deployment sequencing this stack assumes
/// (the watchtower is enabled before any reserved redemption is
/// requested).
function raiseReservedObjection(uint256 reservationKey, uint64 requestNonce)
external
onlyGuardian
{
uint256 vetoKey = reservedVetoKey(reservationKey, requestNonce);
VetoProposal storage veto = vetoProposals[vetoKey];

require(
veto.objectionsCount < REQUIRED_OBJECTIONS_COUNT,
"Reserved redemption already vetoed"
);

uint256 objectionKey = uint256(
keccak256(abi.encodePacked(vetoKey, msg.sender))
);
require(!objections[objectionKey], "Guardian already objected");

Reservation.ReservationAction memory action = IReservationBridge(
address(bridge)
).reservationActions(reservationKey, requestNonce);

require(
action.actionType == Reservation.ActionType.Redemption &&
action.state == Reservation.ActionState.Pending,
"Reserved redemption does not exist"
);

require(
/* solhint-disable-next-line not-rely-on-time */
block.timestamp <
uint256(action.requestedAt) +
_reservedRedemptionDelay(action, veto.objectionsCount),
"Redemption veto delay period expired"
);

objections[objectionKey] = true;
veto.redeemer = action.redeemer;
veto.objectionsCount++;

emit ObjectionRaised(vetoKey, msg.sender);

if (veto.objectionsCount == REQUIRED_OBJECTIONS_COUNT) {
uint64 penaltyFee = vetoPenaltyFeeDivisor > 0
? action.amount / vetoPenaltyFeeDivisor
: 0;

veto.withdrawableAmount = action.amount - penaltyFee;
/* solhint-disable-next-line not-rely-on-time */
veto.finalizedAt = uint32(block.timestamp);
isBanned[action.redeemer] = true;

emit Banned(action.redeemer);

emit VetoFinalized(vetoKey);

// Notify the Bridge about the veto. As result of this call,
// this contract receives the escrowed gross amount
// (as Bank's balance) from the Bridge.
IReservationBridge(address(bridge)).notifyReservedRedemptionVeto(
reservationKey,
requestNonce
);
// Burn the penalty fee but leave the claimable amount for the
// redeemer to withdraw after the freeze period.
bank.decreaseBalance(penaltyFee);
}
}

/// @notice Returns the applicable veto delay for the given pending
/// reserved redemption generation. This is the delay the
/// Bridge proof path enforces before a redemption generation
/// can settle: wallets must not sign before it elapses.
/// @param reservationKey The key of the reservation.
/// @param requestNonce The redemption generation.
/// @return Reserved redemption veto delay.
/// @dev The returned level is selected from the immutable schedule the
/// Bridge captured when the generation was requested. A permanently
/// disabled watchtower overrides every captured schedule with zero.
function getReservedRedemptionDelay(
uint256 reservationKey,
uint64 requestNonce
) external view returns (uint32) {
// Preserve the pre-enable API behavior: no guardians exist, so no
// reservation lookup is needed to establish that the delay is zero.
// slither-disable-next-line incorrect-equality
if (watchtowerEnabledAt == 0) {
return 0;
}

Reservation.ReservationAction memory action = IReservationBridge(
address(bridge)
).reservationActions(reservationKey, requestNonce);

require(
action.actionType == Reservation.ActionType.Redemption,
"Reserved redemption does not exist"
);

return
_reservedRedemptionDelay(
action,
vetoProposals[reservedVetoKey(reservationKey, requestNonce)]
.objectionsCount
);
}

/// @notice Returns the current three-level veto delay schedule to
/// snapshot for a newly requested reserved redemption.
/// @param requestedAmount Reserved redemption amount in satoshis.
/// @return reservedDefaultDelay Delay before any guardian objection.
/// @return reservedLevelOneDelay Delay after one guardian objection.
/// @return reservedLevelTwoDelay Delay after two guardian objections.
/// @dev Returns an all-zero schedule when no guardian veto policy applies
/// at request time: the watchtower is not enabled, has been disabled,
/// or the requested amount is below the waiver limit.
function getReservedRedemptionDelaySchedule(uint64 requestedAmount)
external
view
returns (
uint32 reservedDefaultDelay,
uint32 reservedLevelOneDelay,
uint32 reservedLevelTwoDelay
)
{
if (
// slither-disable-next-line incorrect-equality
watchtowerEnabledAt == 0 ||
watchtowerDisabledAt != 0 ||
requestedAmount < waivedAmountLimit
) {
return (0, 0, 0);
}

return (defaultDelay, levelOneDelay, levelTwoDelay);
}

/// @notice Selects a reserved redemption generation's snapshotted veto
/// delay for the given number of objections.
/// @dev A permanently disabled watchtower makes the effective delay zero
/// for every generation, including those requested before shutdown.
function _reservedRedemptionDelay(
Reservation.ReservationAction memory action,
uint8 objectionsCount
) internal view returns (uint32) {
if (watchtowerDisabledAt != 0) {
return 0;
}

if (
// slither-disable-next-line incorrect-equality
objectionsCount == 0
) {
return action.watchtowerDefaultDelay;
} else if (
// slither-disable-next-line incorrect-equality
objectionsCount == 1
) {
return action.watchtowerLevelOneDelay;
} else if (
// slither-disable-next-line incorrect-equality
objectionsCount == 2
) {
return action.watchtowerLevelTwoDelay;
} else {
revert("No delay for given objections count");
}
}

/// @notice Determines whether a reserved redemption request is
/// considered safe: neither the reservation owner nor the
/// redeemer may be banned. Objection state is per generation,
/// so no historical objection count is consulted -- a fresh
/// generation always starts with a clean count.
/// @param owner The reservation owner.
/// @param redeemer The address requesting the redemption.
/// @return True if the reserved redemption request is safe.
function isSafeReservedRedemption(address owner, address redeemer)
external
view
returns (bool)
{
return !isBanned[owner] && !isBanned[redeemer];
}

/// @notice Updates the watchtower parameters.
/// @param _watchtowerLifetime Duration of the watchtower lifetime in seconds.
/// @param _vetoPenaltyFeeDivisor Divisor used to compute the redemption veto
Expand Down
Loading
Loading