CompliantDepositRegistry

Purpose

CompliantDepositRegistry assigns one BTC deposit address to each compliant investor address. Deposit addresses are pre-generated off-chain by the custodian and added to the registry in batches that are temporarily challengeable. Investors self-register (idempotent), and the registry enforces a compliance check via an external IComplianceChecker. This contract is used for registering the BTC Deposit Address for BTC Native Deposits to Mint hBTC.

Imports

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {IComplianceChecker} from "./interfaces/IComplianceChecker.sol";
import {ICompliantDepositRegistry} from "./interfaces/ICompliantDepositRegistry.sol";
  • AccessControl — role-based permissions for admin, batch creators, and cancelers.

  • IComplianceChecker — on-chain compliance gate; exposes isCompliant/requireCompliant.

  • ICompliantDepositRegistry — declares the registry’s custom errors/events/interfaces used here.

Roles

bytes32 public constant DEPOSIT_ADDRESS_CREATOR_ROLE = keccak256("DEPOSIT_ADDRESS_CREATOR_ROLE");
bytes32 public constant CANCELER_ROLE               = keccak256("CANCELER_ROLE");
  • DEFAULT_ADMIN_ROLE (AccessControl): can grant/revoke roles; can set the challenge period.

  • DEPOSIT_ADDRESS_CREATOR_ROLE: may push new batches of addresses.

  • CANCELER_ROLE: may challenge (cancel) the latest batch during its challenge window.

Storage Layout

mapping(address investor => uint depositAddressIndex) public investorDepositMap;

string[]  public depositAddresses;      // [0] is a sentinel "Blocked entry"
uint256   public nextDepositAddressIndex; // starts at 1

uint256   public batchChallengePeriod;    // seconds
uint256   public latestBatchUnlockTime;   // timestamp when challenge window ends
uint256   public finalizedAddressesLength;// array length at the moment the latest batch was added

IComplianceChecker public immutable complianceChecker;

Invariants / Design Notes

  • Index 0 is blocked by design so that “zero” in investorDepositMap unambiguously means not registered.

  • nextDepositAddressIndex starts at 1.

  • At addDepositAddresses(...), we set finalizedAddressesLength = depositAddresses.length (the “start” of the new batch). All addresses with index >= finalizedAddressesLength form the latest batch and remain challengeable until latestBatchUnlockTime.

  • Investors can only be assigned addresses from finalized space unless the challenge window passed for the latest batch.

Constructor

constructor(
  address defaultAdmin,
  address complianceCheckerAddress,
  address cancelerAddress
) {
  _grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
  _grantRole(CANCELER_ROLE, cancelerAddress);

  complianceChecker = IComplianceChecker(complianceCheckerAddress);

  depositAddresses.push("Blocked entry");
  nextDepositAddressIndex = 1;

  _setBatchChallengePeriod(1 days);
}
  • Sets admin and canceller.

  • Pins complianceChecker.

  • Inserts a sentinel into depositAddresses[0].

  • Defaults batchChallengePeriod to 1 day.

Events (emitted)

Event types are declared in ICompliantDepositRegistry. Names and payloads below match the emit sites in your code.

  • event DepositAddressSet(address indexed investor, string depositAddress);

  • event NewDepositAddressesAdded(uint256 indexed startIndex, uint256 unlockTime, string[] newDepositAddresses);

  • event BatchChallengePeriodSet(uint256 newChallengePeriod);

  • event BatchChallenged(uint256 indexed finalizedStart, uint256 timestamp, uint256 removed);

Errors (thrown)

Declared in ICompliantDepositRegistry / IComplianceChecker and used here:

  • error UnregisteredInvestor();

  • error OutOfDepositAddresses();

  • error NoNewAddressesDuringChallengePeriod();

  • error NoChallengeAfterUnlock();

  • error ComplianceCheckFailed(); (bubbled from complianceChecker.requireCompliant)


Public / External

getDepositAddress

function getDepositAddress(address investor) public view returns (string memory);

What it does

  • Returns the assigned BTC deposit address for investor.

Requirements / Reverts

  • Reverts UnregisteredInvestor() if investorDepositMap[investor] == 0.

  • Calls complianceChecker.requireCompliant(investor) and reverts ComplianceCheckFailed() if not compliant at read time.

Returns

  • The BTC deposit address string from depositAddresses[investorDepositIndex].

Notes

  • The compliance check happens on every read to actively block non-compliant users from depositing even if previously registered.


hasRegisteredDepositAddress

function hasRegisteredDepositAddress(address investor) public view returns (bool);

Returns true iff the investor already has a non-zero index in investorDepositMap.


registerDepositAddress

function registerDepositAddress() public returns (string memory depositAddress);

What it does

  • Idempotent investor self-registration.

  • If the caller already has an index, it returns the existing address (after a compliance check inside getDepositAddress).

  • Otherwise, assigns the next available finalized address.

Flow

  1. If investorDepositMap[msg.sender] > 0return getDepositAddress(msg.sender).

  2. Let _next = nextDepositAddressIndex.

  3. If _next >= finalizedAddressesLength, enforce:

    • _next < depositAddresses.length AND

    • latestBatchUnlockTime < block.timestamp Else revert OutOfDepositAddresses().

  4. complianceChecker.requireCompliant(msg.sender).

  5. Assign: investorDepositMap[msg.sender] = _next; nextDepositAddressIndex++.

  6. depositAddress = getDepositAddress(msg.sender);

  7. emit DepositAddressSet(msg.sender, depositAddress).

Returns

  • The assigned BTC deposit address string.

Operational note

  • Your in-code doc comment recommends waiting for several confirmations off-chain before surfacing the address to the user.


depositAddressesLength

function depositAddressesLength() public view returns (uint);

Returns depositAddresses.length. (Includes index 0 sentinel.)


getDepositAddresses

function getDepositAddresses(uint256 startIndex, uint256 count)
  public view returns (string[] memory addresses);

Reads a page (slice) of addresses from the array. If the requested count overshoots, the function trims to available items.


addDepositAddresses

function addDepositAddresses(string[] calldata newDepositAddresses)
  public onlyRole(DEPOSIT_ADDRESS_CREATOR_ROLE);

What it does

  • Appends a new batch of deposit addresses.

  • Finalizes all previously existing addresses by setting finalizedAddressesLength = depositAddresses.length (the pre-append length).

  • Opens a challenge window for the new batch until block.timestamp + batchChallengePeriod.

Requirements / Reverts

  • Reverts NoNewAddressesDuringChallengePeriod() if the previous batch is still challengeable (latestBatchUnlockTime >= block.timestamp).

Effects

  • Pushes each new address into depositAddresses.

  • Sets latestBatchUnlockTime = block.timestamp + batchChallengePeriod.

  • Emits:

    NewDepositAddressesAdded(startIndex, latestBatchUnlockTime, newDepositAddresses)

    where startIndex is the pre-append length (== new batch start).


setBatchChallengePeriod

function setBatchChallengePeriod(uint256 newChallengePeriod)
  public onlyRole(DEFAULT_ADMIN_ROLE);

Sets the challenge window length (seconds). Emits BatchChallengePeriodSet(newChallengePeriod).


challengeLatestBatch (overload #1)

function challengeLatestBatch() public onlyRole(CANCELER_ROLE);

Convenience overload. Computes the size of the latest batch as:

depositAddresses.length - finalizedAddressesLength

and calls the parameterized overload below.


challengeLatestBatch (overload #2)

function challengeLatestBatch(uint256 length) public onlyRole(CANCELER_ROLE);

What it does

  • Cancels (pops) length addresses from the end of depositAddresses, i.e., from the still-challengeable “latest batch”.

Requirements / Reverts

  • Reverts NoChallengeAfterUnlock() if the challenge window has already expired:

    require(latestBatchUnlockTime >= block.timestamp, NoChallengeAfterUnlock())

    (I.e., you can only challenge until latestBatchUnlockTime.)

Effects

  • for (i=0; i<length; i++) depositAddresses.pop();

  • Sets latestBatchUnlockTime = block.timestamp; (resets so a new batch can be added)

  • Emits:

    BatchChallenged(finalizedAddressesLength, block.timestamp, length);

Notes

  • The helper without args challenges the entire latest batch. This overload lets you challenge a subset if you choose.


Access Control Summary

  • DEFAULT_ADMIN_ROLE

    • Grant/revoke roles.

    • setBatchChallengePeriod(...).

  • DEPOSIT_ADDRESS_CREATOR_ROLE

    • addDepositAddresses(...).

  • CANCELER_ROLE

    • challengeLatestBatch(...).

Access control events (RoleGranted, RoleRevoked, RoleAdminChanged) come from OpenZeppelin’s AccessControl.

Integration Pattern (Typical)

  1. Custodian/off-chain system generates and verifies a list of BTC addresses.

  2. Address creator calls addDepositAddresses(batch). A challenge window opens.

  3. If the batch is valid, do nothing; once the window elapses, those addresses become assignable. If a problem is detected, a canceller calls challengeLatestBatch() before latestBatchUnlockTime.

  4. Investors call registerDepositAddress(). The registry enforces compliance and assigns the next finalized address.

  5. Downstream systems use getDepositAddress(user) to read the address (compliance-gated at read time).

Security Considerations

  • Compliance at read time: getDepositAddress re-checks compliance so a user that becomes non-compliant cannot fetch the address to deposit.

  • Batch challenge: protects against bad address uploads; only the latest batch is challengeable.

  • Exhaustion guard: registerDepositAddress reverts if no finalized addresses are available (or the last batch is still challengeable).

  • Sentinel index: prevents accidental assignment of an uninitialized (zero) index

Last updated