Skip to content
Merged
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
7 changes: 5 additions & 2 deletions ethereum/script/deploy/DeployAll.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import { RgbSettlementModule } from '../../src/settlement/RgbSettlementModule.so
/// PRIVATE_KEY, USDT0_ADDRESS, BTC_RELAY_ADDRESS,
/// ENCLAVE_SIGNERS, ENCLAVE_THRESHOLD,
/// FEDERATION_SIGNERS, FEDERATION_THRESHOLD,
/// COMMISSION_RECIPIENT, TIMELOCK_DURATION, MIN_TIMELOCK
/// COMMISSION_RECIPIENT, TIMELOCK_DURATION, MIN_TIMELOCK,
/// MIN_FUNDS_IN_AMOUNT
///
/// Env (optional):
/// ETH_USD_FEED — Chainlink ETH/USD aggregator (wired in before CM
Expand Down Expand Up @@ -74,6 +75,7 @@ contract DeployAll is Script {
uint256 minTimelock = vm.envUint('MIN_TIMELOCK');
address ethUsdFeed = vm.envOr('ETH_USD_FEED', address(0));
uint256 ethUsdHb = vm.envOr('ETH_USD_HEARTBEAT', uint256(0));
uint256 minFundsIn = vm.envUint('MIN_FUNDS_IN_AMOUNT');

address deployer = vm.addr(pk);
uint64 startNonce = vm.getNonce(deployer);
Expand All @@ -96,7 +98,8 @@ contract DeployAll is Script {
usdt0,
address(routeRegistry),
payable(address(cm)),
address(0)
address(0),
minFundsIn
);

// ---- 5. Route plugins (nonce n+3, n+4) ---------------------------
Expand Down
10 changes: 9 additions & 1 deletion ethereum/script/deploy/DeployBridge.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import { Bridge } from '../../src/Bridge.sol';
/// `Bridge.setLZAdapter(adapter)`. The Bridge
/// accepts adapter-only `fundsInFromAdapter`
/// calls *only* from the configured adapter.
/// MIN_FUNDS_IN_AMOUNT — Minimum accepted `fundsIn` deposit in token
/// smallest units (USDT0 has 6 decimals, so e.g.
/// 10000 = 0.01 USDT0). Required and must be
/// non-zero; retune later via federation
/// governance with
/// `Bridge.setMinFundsInAmount(newMinimum)`.
///
/// Usage:
/// forge script script/deploy/DeployBridge.s.sol \
Expand All @@ -40,9 +46,10 @@ contract DeployBridge is Script {
address routeRegistry = vm.envAddress('ROUTE_REGISTRY_ADDRESS');
address commissionManager = vm.envAddress('COMMISSION_MANAGER');
address lzAdapter = vm.envOr('LZ_ADAPTER', address(0));
uint256 minFundsInAmount = vm.envUint('MIN_FUNDS_IN_AMOUNT');

vm.startBroadcast(pk);
bridge = new Bridge(usdt0, routeRegistry, payable(commissionManager), lzAdapter);
bridge = new Bridge(usdt0, routeRegistry, payable(commissionManager), lzAdapter, minFundsInAmount);
vm.stopBroadcast();

console2.log('Bridge deployed at: ', address(bridge));
Expand All @@ -51,5 +58,6 @@ contract DeployBridge is Script {
console2.log('RouteRegistry: ', address(bridge.routeRegistry()));
console2.log('CommissionManager: ', address(bridge.commissionManager()));
console2.log('LZ adapter: ', bridge.lzAdapter());
console2.log('Min fundsIn amount: ', bridge.minFundsInAmount());
}
}
22 changes: 22 additions & 0 deletions ethereum/src/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ contract Bridge is BridgeBase, IBridge, ReentrancyGuard {
// State
// =========================================================================

/// @notice Upper bound on the `destinationAddress` (fundsIn) and
/// `sourceAddress` (fundsOut) byte length. These strings are echoed
/// into event logs, so an unbounded value would let a caller inflate
/// log-storage and indexer cost. 512 bytes covers every supported
/// destination representation (RGB invoices and other chains) with
/// ample headroom.
uint256 public constant MAX_ADDRESS_LENGTH = 512;

/// @notice Upper bound on the `fundsOut` `proof` byte length. `proof` is
/// forwarded to the route verifier, so an unbounded blob would let a
/// release inflate calldata + verifier gas. 1024 bytes comfortably
/// fits the verifier formats (RGB is 64 bytes today; a future SPV
/// inclusion proof stays well under this).
uint256 public constant MAX_PROOF_LENGTH = 1024;

/// @notice CommissionManager that receives and custodies protocol fees.
ICommissionManager public immutable commissionManager;

Expand Down Expand Up @@ -203,6 +218,10 @@ contract Bridge is BridgeBase, IBridge, ReentrancyGuard {
if (recipient == address(0)) revert InvalidRecipientAddress();
if (sourceChainId == 0) revert InvalidSourceChainId();
if (destinationChainId == 0) revert InvalidDestinationChainId();
if (bytes(sourceAddress).length > MAX_ADDRESS_LENGTH) {
revert AddressTooLong(bytes(sourceAddress).length, MAX_ADDRESS_LENGTH);
}
if (proof.length > MAX_PROOF_LENGTH) revert ProofTooLong(proof.length, MAX_PROOF_LENGTH);
if (amount > IERC20(TOKEN).balanceOf(address(this))) revert AmountExceedBridgePool();

// Common replay guard. Set the flag before any external interaction
Expand Down Expand Up @@ -290,6 +309,9 @@ contract Bridge is BridgeBase, IBridge, ReentrancyGuard {
) private {
if (amount < minFundsInAmount) revert AmountBelowMinimum(amount, minFundsInAmount);
if (bytes(destinationAddress).length == 0) revert InvalidDestinationAddress();
if (bytes(destinationAddress).length > MAX_ADDRESS_LENGTH) {
revert AddressTooLong(bytes(destinationAddress).length, MAX_ADDRESS_LENGTH);
}
if (sourceChainId == 0) revert InvalidSourceChainId();
if (destinationChainId == 0) revert InvalidDestinationChainId();

Expand Down
63 changes: 59 additions & 4 deletions ethereum/src/MultisigProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,22 @@ contract MultisigProxy is IMultisigProxy {
/// @notice Maximum allowed time between proposal creation and its deadline.
uint256 public constant MAX_PROPOSAL_LIFETIME = 30 days;

/// @notice Maximum allowed lifetime of a TEE-signed `execute` / `executeBatch`
/// deadline. Tighter than `MAX_PROPOSAL_LIFETIME` because enclave
/// operations (e.g. `fundsOut`) are meant to be executed promptly
/// after signing; a short ceiling limits how long a leaked or
/// pre-signed payload stays executable while its nonce is unconsumed.
uint256 public constant MAX_TEE_DEADLINE = 1 days;

/// @notice Hard upper bound on `executeBatch` size to keep gas use bounded.
uint256 public constant MAX_BATCH_SIZE = 3;

/// @notice Hard upper bound on the size of either signer set. Bounds the
/// O(N^2) duplicate/disjointness checks and stays far within the
/// `uint256` signature-bitmap width (so no signer index is ever
/// unreachable). 20 is ample for an enclave/federation committee.
uint256 public constant MAX_SIGNERS = 20;

/// @notice Length of a Solidity function selector (`bytes4`) in bytes. Used
/// to guard `callData` against payloads too short to even carry a
/// selector before it is sliced via `calldataload`.
Expand Down Expand Up @@ -176,16 +189,19 @@ contract MultisigProxy is IMultisigProxy {
if (bridge_ == address(0)) revert ZeroBridge();
if (commissionManager_ == address(0)) revert ZeroCommissionManager();
if (enclaveSigners_.length == 0) revert NoSigners();
if (enclaveThreshold_ == 0 || enclaveThreshold_ > enclaveSigners_.length) revert InvalidThreshold();
_requireValidThreshold(enclaveThreshold_, enclaveSigners_.length);
if (federationSigners_.length == 0) revert NoSigners();
if (federationThreshold_ == 0 || federationThreshold_ > federationSigners_.length) revert InvalidThreshold();
_requireValidThreshold(federationThreshold_, federationSigners_.length);
if (commissionRecipient_ == address(0)) revert ZeroCommissionRecipient();
if (minTimelock_ == 0 || minTimelock_ >= MAX_PROPOSAL_LIFETIME) revert InvalidMinTimelock();
if (timelockDuration_ < minTimelock_) revert TimelockTooShort();
if (timelockDuration_ >= MAX_PROPOSAL_LIFETIME) revert TimelockTooLong();

_validateSigners(enclaveSigners_);
_validateSigners(federationSigners_);
// The enclave and federation are distinct trust domains; an address in
// both would count toward quorum in each, collapsing that separation.
_requireDisjoint(enclaveSigners_, federationSigners_);

bridge = bridge_;
commissionManager = commissionManager_;
Expand Down Expand Up @@ -228,6 +244,10 @@ contract MultisigProxy is IMultisigProxy {
bytes[] calldata enclaveSigs
) external {
if (block.timestamp > deadline) revert Expired();
// Bound how far in the future a signed deadline may sit, so a leaked or
// pre-signed payload cannot stay executable indefinitely while its
// nonce is unconsumed.
if (deadline > block.timestamp + MAX_TEE_DEADLINE) revert DeadlineTooFar();
if (callData.length < SELECTOR_LENGTH) revert CallDataTooShort();

bytes4 selector;
Expand Down Expand Up @@ -258,6 +278,8 @@ contract MultisigProxy is IMultisigProxy {
bytes[] calldata enclaveSigs
) external payable {
if (block.timestamp > deadline) revert Expired();
// Bound the signed deadline's distance into the future;
if (deadline > block.timestamp + MAX_TEE_DEADLINE) revert DeadlineTooFar();
uint256 n = targets.length;
if (n == 0) revert BatchEmpty();
if (n > MAX_BATCH_SIZE) revert BatchTooLarge();
Expand Down Expand Up @@ -818,6 +840,7 @@ contract MultisigProxy is IMultisigProxy {
) private returns (bytes32 proposalId) {
if (block.timestamp > deadline) revert Expired();
if (deadline > block.timestamp + MAX_PROPOSAL_LIFETIME) revert DeadlineTooFar();
if (deadline < block.timestamp + timelockDuration) revert DeadlineBeforeTimelock();
if (nonce != proposalNonce) revert InvalidNonce();

bytes32 digest = _hashTypedData(structHash);
Expand Down Expand Up @@ -855,17 +878,19 @@ contract MultisigProxy is IMultisigProxy {
} else if (opType == OperationType.UpdateEnclaveSigners) {
(address[] memory newSigners, uint256 newThreshold) = abi.decode(opData, (address[], uint256));
if (newSigners.length == 0) revert NoSigners();
if (newThreshold == 0 || newThreshold > newSigners.length) revert InvalidThreshold();
_requireValidThreshold(newThreshold, newSigners.length);
_validateSigners(newSigners);
_requireDisjoint(newSigners, _federationSigners);
_enclaveSigners = newSigners;
enclaveThreshold = newThreshold;
emit EnclaveSignersUpdated(newSigners, newThreshold);

} else if (opType == OperationType.UpdateFederationSigners) {
(address[] memory newSigners, uint256 newThreshold) = abi.decode(opData, (address[], uint256));
if (newSigners.length == 0) revert NoSigners();
if (newThreshold == 0 || newThreshold > newSigners.length) revert InvalidThreshold();
_requireValidThreshold(newThreshold, newSigners.length);
_validateSigners(newSigners);
_requireDisjoint(newSigners, _enclaveSigners);
_federationSigners = newSigners;
federationThreshold = newThreshold;
emit FederationSignersUpdated(newSigners, newThreshold);
Expand Down Expand Up @@ -1064,7 +1089,37 @@ contract MultisigProxy is IMultisigProxy {
// =========================================================================

/// @dev Validates no zero addresses and no duplicates. O(n^2), fine for <20 signers.
/// @dev Enforces the quorum policy shared by both signer sets (enclave and
/// federation), in the constructor and the signer-update handlers:
/// - at least 2 signers (no single-key set);
/// - threshold of at least 2 (no single signer can authorise alone);
/// - threshold a strict majority (`2 * threshold > n`), which rejects
/// 1-of-N and any sub-majority quorum;
/// - threshold not exceeding the signer count.
/// The smallest valid sets are 2-of-2 and 2-of-3. This upholds the
/// spec invariant "one signer MUST NOT authorise fundsOut alone".
function _requireValidThreshold(uint256 threshold, uint256 n) private pure {
if (n < 2 || threshold < 2 || threshold > n || 2 * threshold <= n) {
revert InvalidThreshold();
}
}

/// @dev Reverts if any address in `candidate` also appears in `counterpart`.
/// Used to keep the enclave and federation signer sets disjoint so the
/// two trust domains stay independent (R-W-14). O(n*m), bounded by the
/// signer-set sizes.
function _requireDisjoint(address[] memory candidate, address[] memory counterpart) private pure {
for (uint256 i = 0; i < candidate.length; i++) {
for (uint256 j = 0; j < counterpart.length; j++) {
if (candidate[i] == counterpart[j]) revert SignerSetsOverlap(candidate[i]);
}
}
}

function _validateSigners(address[] memory signers) private pure {
// Bound the set before the O(N^2) duplicate scan below, and keep every
// signer index within the uint256 signature bitmap (R-I-06).
if (signers.length > MAX_SIGNERS) revert TooManySigners(signers.length, MAX_SIGNERS);
for (uint256 i = 0; i < signers.length; i++) {
if (signers[i] == address(0)) revert ZeroAddressSigner();
for (uint256 j = i + 1; j < signers.length; j++) {
Expand Down
15 changes: 2 additions & 13 deletions ethereum/src/interfaces/IBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,11 @@ interface IBridge {
error InvalidDestinationAddress();
error InvalidDestinationChainId();
error InvalidSourceChainId();
/// @notice `fundsOut` was called with `amount == 0`. A zero-amount release
/// is a no-op that only emits a redundant event, so it is rejected.
/// (The inbound path is guarded by `AmountBelowMinimum` instead,
/// since its non-zero `minFundsInAmount` already excludes zero.)
error ZeroAmount();
/// @notice A `fundsIn` deposit was below the configured `minFundsInAmount`.
/// The minimum is always non-zero, so this also rejects zero-amount
/// deposits, and it keeps dust — small enough that its commission
/// rounds to zero and only adds event/processing noise — off the
/// inbound path. Does not apply to `fundsOut` (authorized releases
/// of already-recorded amounts only check `amount > 0`).
error AmountBelowMinimum(uint256 amount, uint256 minimum);
/// @notice `minFundsInAmount` was set to zero at construction or via
/// `setMinFundsInAmount`. The floor must be non-zero so that it
/// meaningfully rejects zero-amount and dust deposits.
error InvalidMinFundsInAmount();
error AddressTooLong(uint256 length, uint256 maxLength);
error ProofTooLong(uint256 length, uint256 maxLength);
error InvalidRouteRegistryAddress();
error InvalidCommissionManagerAddress();
error NotLZAdapter();
Expand Down
3 changes: 3 additions & 0 deletions ethereum/src/interfaces/IMultisigProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface IMultisigProxy {
error ProposalExpired();
error DataMismatch();
error DeadlineTooFar();
error DeadlineBeforeTimelock();
error ProposalExists();
error IndexOutOfRange();
error BitmapOutOfRange();
Expand All @@ -62,6 +63,8 @@ interface IMultisigProxy {
error InvalidSignature();
error ZeroAddressSigner();
error DuplicateSigner();
error SignerSetsOverlap(address signer);
error TooManySigners(uint256 count, uint256 max);
error CallFailed();
error UnknownOperationType();
error ZeroRecipient();
Expand Down
Loading