> For the complete documentation index, see [llms.txt](https://docs.selcier.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.selcier.xyz/build-on-selcier/smart-contracts.md).

# Smart contract map

How Selcier's Solidity contracts fit together on Robinhood Chain, which functions matter on each one, and where to look up a company's deployed addresses.

Everything Selcier does on Robinhood Chain happens in Solidity. A single factory stamps out a separate, self-contained set for each company, and each share class inside that company receives its own token, rule engine and valuation oracle. Isolation is strict: nothing deployed for one company can read or alter the state of another.

```
SelcierFactory
 └── per company
      ├── IdentityRegistry ◄── TrustedVerifiers
      ├── OfferingVault
      ├── LiquidityWindow
      ├── RFQSettlement
      ├── DistributionVault
      ├── Governance
      ├── DisclosureRegistry
      ├── IssuerTimelock
      └── per share class
           ├── SelcierShareToken
           ├── RuleEngine ──► modules
           ├── ValuationOracle
           └── PoolGate (if permissioned pools are enabled)
```

## Locating deployed addresses

These docs do not print addresses. Two places are authoritative instead:

| Source                                       | What it gives you                                                                                                                                                                                       |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The API                                      | On whichever network you are calling, `GET /v1/contracts` answers with module implementations and the factory address. Scope that to one company's entire set with `GET /v1/contracts?company=cmp_...`. |
| The [Explorer](https://explorer.selcier.xyz) | Open a company to see its list of contracts. From there, links lead to the verified code on Robinhood Chain's block explorer.                                                                           |

The chain itself offers a third route. For each company the factory emits `CompanyDeployed`, and for each added share class it emits `ShareClassDeployed`. Follow those two events from the factory address and you end up with the whole contract set of all issuers.

```solidity
event CompanyDeployed(
    bytes32 indexed companyId,
    address identityRegistry,
    address trustedVerifiers,
    address offeringVault,
    address liquidityWindow,
    address rfqSettlement,
    address distributionVault,
    address governance,
    address disclosureRegistry,
    address issuerTimelock
);

event ShareClassDeployed(
    bytes32 indexed companyId,
    bytes32 indexed shareClass,
    address token,
    address ruleEngine,
    address valuationOracle
);

event PoolGateDeployed(bytes32 indexed companyId, address indexed token, address poolGate);
```

`deployCompany` and `deployShareClass` are restricted to the factory owner, which is Selcier, and the factory keeps no role on anything it deploys. To look up the engine, oracle and gate behind one share class, use `shareClassOf(token)`.

Blockscout, at `robinhoodchain.blockscout.com`, carries verified source for all of these contracts.

## Ownership and compliance

### Share token: SelcierShareToken

A share class maps to exactly one token. That token is an ordinary ERC-20 with 18 decimals, and it also lines up with ERC-3643 (permissioned transfers) and ERC-8056 (corporate-action scaling). Whichever path a transfer takes, be it `transfer`, `transferFrom` or a Selcier settlement contract, it goes through `canTransfer`, and a failed check reverts using the reason code that call produced.

A UUPS proxy fronts the token. Seven days must pass after an upgrade is proposed, and the proposal itself has to come through `IssuerTimelock`.

```solidity
interface ISelcierShareToken is IERC20, IERC3643, IERC8056 {
    // Identity & transfer rules
    function identityRegistry() external view returns (IIdentityRegistry);
    function ruleEngine() external view returns (IRuleEngine);
    function canTransfer(address from, address to, uint256 amount)
        external view returns (bool ok, bytes32 reasonCode);

    // Corporate actions (ERC-8056)
    function uiMultiplier() external view returns (uint256); // 1e18 = 1.0

    function isAgent(address account) external view returns (bool);

    // Issuer controls (role-gated, timelocked where noted, all emit events)
    function mint(address to, uint256 amount, bytes32 ref) external;
    function burn(address from, uint256 amount, bytes32 ref) external;
    function approveRecovery(address lost, address replacement, bytes32 ref) external;
    function cancelRecovery(address lost) external;
    function recover(address lost, address replacement, bytes32 ref) external;
    function freezePartial(address holder, uint256 amount, bytes32 reason) external;
    function unfreezePartial(address holder, uint256 amount) external;
    function pause(bytes32 reason) external;
    function unpause() external;
    function scheduleMultiplier(uint256 newMultiplier, uint64 effectiveAt) external;
    function applyMultiplier() external;

    // Metadata
    function shareClass() external view returns (bytes32);
    function companyId() external view returns (bytes32);
    function documentsRoot() external view returns (bytes32);
    function unlockedBalanceOf(address holder) external view returns (uint256);

    // Snapshots (L2 block numbers on Robinhood Chain)
    function clock() external view returns (uint48);
    function balanceOfAt(address account, uint48 timepoint) external view returns (uint256);
    function totalSupplyAt(uint48 timepoint) external view returns (uint256);
}
```

The `ref` argument is a free-form `bytes32` that ties an action to its off-chain record, for example a document hash or a cap table entry. Solidity reserves the word `reference`, hence the shorter name.

| Function                                 | Permitted caller                               | Behavior                                                                                                                               |
| ---------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `mint`                                   | `REGISTRAR`, or `MINTER` (the `OfferingVault`) | The recipient goes through the complete rule check                                                                                     |
| `burn`                                   | `REGISTRAR`                                    | Use `ref` to point at the cap table record and the hash of its document                                                                |
| `approveRecovery`                        | `ISSUER_ADMIN`, `REGISTRAR` or `OPERATOR`      | Once a second, different party approves, the recovery is scheduled. The delay defaults to 72 hours and cannot go under a 48-hour floor |
| `cancelRecovery`                         | `ISSUER_ADMIN`, `REGISTRAR` or `OPERATOR`      | Execution is the only cutoff: until then, a pending recovery can be called off                                                         |
| `recover`                                | `REGISTRAR`                                    | Moves the balance, provided a recovery was scheduled and its waiting time is over                                                      |
| `freezePartial`, `unfreezePartial`       | `REGISTRAR`                                    | The tokens remain in the holder's wallet, immovable while frozen                                                                       |
| `pause`, `unpause`                       | `PAUSER`                                       | Takes effect at once. While paused, no transfer goes through and the valuation oracle stops                                            |
| `scheduleMultiplier`, `cancelMultiplier` | `ISSUER_ADMIN`                                 | Effective time has to sit 48 hours or more in the future                                                                               |
| `applyMultiplier`                        | Open to all                                    | Emits `UIMultiplierUpdated` to finalize a multiplier whose effective time has arrived                                                  |
| `unlockedBalanceOf`                      | Open to all                                    | Reports the part of a balance that freezes, holding periods and vesting leave free                                                     |
| `isAgent`                                | Open to all                                    | Returns true for two kinds of account: the company's `LiquidityWindow`, and contracts with a valid `VENUE` claim naming this token     |

Calls to `setAgent`, `setDocumentsRoot`, `setRecoveryDelay`, `setMultiplierDelay` and `upgradeToAndCall` come from `IssuerTimelock` and nowhere else, and a delay beneath its floor is refused.

Corporate actions leave raw balances untouched. To convert them to shares, multiply by `uiMultiplier() / 1e18`. [Shareholder events](/fundamentals/shareholder-events.md) has the background.

### Wallet identities: IdentityRegistry

Each company has one registry, and it ties wallets to on-chain identities. Signed claims live on the identity, and a claim is accepted only when its signer appears in `TrustedVerifiers` for the topic in question.

| What it does                                                                                                                                                                                                                     | Signature                                                                                                 |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Ties a wallet to an identity. `OPERATOR` calls it once verification is done                                                                                                                                                      | `registerIdentity(address wallet, address identity)`                                                      |
| Breaks that link. Reserved for `OPERATOR`                                                                                                                                                                                        | `removeIdentity(address wallet)`                                                                          |
| Looks up a wallet's identity, returning zero when there is none                                                                                                                                                                  | `identityOf(address wallet) returns (address)`                                                            |
| Records a claim. There are two routes: a relayer submits the verifier's EIP-712 signature, or the verifier calls in person and leaves the signature empty. Replaying a signature fails, since each one is accepted a single time | `addClaim(address identity, bytes32 topic, bytes data, uint64 expiry, address verifier, bytes signature)` |
| Removes a claim. Open to `OPERATOR` and to the verifier behind the claim                                                                                                                                                         | `revokeClaim(address identity, bytes32 topic)`                                                            |
| Returns what is stored: verifier, expiry and data                                                                                                                                                                                | `claimOf(address identity, bytes32 topic) returns (Claim)`                                                |
| Produces the EIP-712 digest for a verifier to sign                                                                                                                                                                               | `claimDigest(address identity, bytes32 topic, bytes data, uint64 expiry) returns (bytes32)`               |
| The identity check run on each transfer. Answers `NOT_VERIFIED`, `CLAIM_EXPIRED` or `VERIFIED`                                                                                                                                   | `status(address wallet) returns (Status)`                                                                 |
| Tells you whether valid `IDENTITY_VERIFIED` and `SCREENED` claims are both present                                                                                                                                               | `isVerified(address wallet) returns (bool)`                                                               |
| Tells you whether a claim is present, unexpired and signed by a trusted verifier                                                                                                                                                 | `hasValidClaim(address wallet, bytes32 topic) returns (bool)`                                             |
| Reads the claim's value, for instance an ISO 3166 code under `REGION`                                                                                                                                                            | `claimData(address wallet, bytes32 topic) returns (bytes)`                                                |

For signed claims, the EIP-712 domain has name `Selcier` and version `1`. The type is:

```solidity
Claim(address identity,bytes32 topic,bytes32 dataHash,uint64 expiry,bytes32 companyId,address registry)
```

The `dataHash` field is a keccak256 taken over the claim data. If a claim's verifier has been dropped from `TrustedVerifiers` since signing, the claim reports `CLAIM_EXPIRED`.

### Verifier allowlist: TrustedVerifiers

This contract records, per company, which verifier keys may sign which claim topics. Selcier keeps up a default set of identity providers, and an issuer changes its own list through `IssuerTimelock`. The functions to know:

* `addVerifier(address verifier, bytes32[] topics)`
* `removeVerifier(address verifier)`
* `isTrusted(address verifier, bytes32 topic)`

### Transfer rules: RuleEngine and its modules

A rule engine belongs to one share class. Calling `check(from, to, amount)` walks the enabled modules in sequence and hands back the first failure. Since modules are standalone contracts, an issuer can add, drop or reconfigure one and leave the token un-upgraded. By default such a change waits 24 hours in `IssuerTimelock` before it takes effect.

* A rejection from `RegionModule` carries `REGION_BLOCKED`
* A rejection from `EligibilityModule` carries `NOT_ELIGIBLE`
* A rejection from `HoldingPeriodModule` carries `HOLDING_PERIOD_ACTIVE`
* A rejection from `HolderCapModule` carries `HOLDER_CAP_REACHED`
* A rejection from `TransferWindowModule` carries `WINDOW_CLOSED`
* A rejection from `ROFRModule` carries `ROFR_PENDING`
* A rejection from `MaxOwnershipModule` carries `MAX_OWNERSHIP_EXCEEDED`
* A rejection from `EmployeeVestingModule` carries `UNVESTED`

On the engine itself you will use:

* `modules() returns (address[])`
* `addModule(address module, bytes config)`
* `removeModule(address module)`
* `configureModule(address module, bytes config)`

For what each module actually enforces, read [Compliance modules](/fundamentals/compliance-modules.md).

## Trading and capital

### Primary raises: OfferingVault

Primary offerings run here. Stablecoin deposits from investors sit in escrow until close, when the vault does one of two things: it opens refunds, or it pays out the funds and mints the shares within a single transaction.

* `createOffering(OfferingParams params) returns (uint256 offeringId)`
* `subscribe(uint256 offeringId, uint256 amount)`
* `close(uint256 offeringId)`
* `cancelOffering(uint256 offeringId)`
* `refund(uint256 offeringId)`
* `state(uint256 offeringId) returns (State)`
* `offering(uint256 offeringId) returns (Offering)`

The ceiling is 500 subscribers per offering (`MAX_SUBSCRIBERS`), and the share class rules are applied to each subscriber a second time at close. More detail is in [Raising primary capital](/markets-and-capital/primary-raises.md).

### Auctions and tenders: LiquidityWindow

Two formats live in this contract, both under issuer control: fixed-price tenders and batch auctions that clear at a uniform price.

* `openWindow(WindowParams params) returns (uint256 windowId)`
* `placeOrder(uint256 windowId, uint8 side, uint256 quantity, uint256 limitPrice) returns (uint256 orderId)`, with `side` set to `0` for `SELL` or `1` for `BUY`
* `cancelOrder(uint256 windowId, uint256 orderId)`
* `removeOrder(uint256 windowId, uint256 orderId)`, which only the company may call, and only after close and before clearing
* `previewClearing(uint256 windowId) returns (uint256 clearingPrice, uint256 volume)`
* `exerciseRofr(uint256 windowId, uint256 quantity)`
* `clear(uint256 windowId)`
* `cancelWindow(uint256 windowId)`
* `claimDeferred(address shareToken)`

`clear` has no caller restriction, but it waits until both the window and any ROFR period are over. Each window takes 200 orders at most (`MAX_ORDERS`). [Scheduled Liquidity Windows](/markets-and-capital/scheduled-windows.md) explains the process.

### Signed quotes: RFQSettlement

This contract settles quotes signed under EIP-712, with payment and delivery happening atomically between two eligible holders. As for the domain, it is made up of the contract address and chain id, plus the name `Selcier` and version `1`.

```solidity
struct Quote {
    address maker;
    address shareToken;
    uint8 side;          // 0 = maker sells, 1 = maker buys
    uint256 quantity;
    uint256 price;       // stablecoin units per share
    address stablecoin;
    address taker;       // zero address = any eligible taker
    uint256 nonce;
    uint64 expiry;
}

function fill(Quote calldata quote, bytes calldata signature, uint256 quantity) external;
function cancelQuote(uint256 nonce) external;
function invalidateNoncesBelow(uint256 newMinNonce) external;
function hashQuote(Quote calldata quote) external view returns (bytes32);
function setEnabled(address shareToken, bool on) external; // ISSUER_ADMIN
```

Signers hash against the following type string:

```solidity
Quote(address maker,address shareToken,uint8 side,uint256 quantity,uint256 price,address stablecoin,address taker,uint256 nonce,uint64 expiry)
```

The trading flow is covered in [Quote-based trading](/markets-and-capital/quote-based-trading.md).

### AMM hooks: PoolGate

`PoolGate` is the hook contract behind permissioned AMM pools. Pools call it through three functions: `beforeSwap(recipient, sharesOut)`, `beforeAddLiquidity(provider)` and `beforeTransferPosition(from, to)`. When the end recipient or the provider does not satisfy the share class rules, the hook reverts with `TransferNotAllowed(reasonCode)`. For a non-reverting answer, `checkSwap` and `checkLiquidityProvider` simply return the reason code.

Registration is done by the company with `registerPool`, going through `IssuerTimelock`. Two limits apply: the pool needs a `VENUE` claim beforehand, and a share class that has a `ROFRModule` cannot register pools at all. [Gated AMM pools](/markets-and-capital/gated-pools.md) has the rest.

## Shareholder services

### Payouts: DistributionVault

Dividends and other proceeds flow through this vault.

* `fund(address shareToken, address asset, uint256 amount, uint48 recordBlock, uint64 claimDeadline, address[] excludedAccounts) returns (uint256 distributionId)`
* `claim(uint256 distributionId)`
* `claimFor(uint256 distributionId, address holder)`
* `claimable(uint256 distributionId, address holder) returns (uint256)`
* `sweep(uint256 distributionId)`

What each holder is owed comes from balances at the record block, a Robinhood Chain L2 block number that has to lie in the past. Up to 20 excluded accounts, for example the `LiquidityWindow` escrow or treasury shares, are taken out of the eligible supply. Claims stay open until the deadline. Setting a deadline is optional, it can be no sooner than 12 months out, and once it passes, `ISSUER_ADMIN` may `sweep` whatever is left to the treasury.

### Voting: Governance

Votes here are weighted by tokens and fixed to a snapshot block.

* `propose(ProposalParams params) returns (uint256 proposalId)`
* `castVote(uint256 proposalId, uint8 option)`
* `castVoteFor(uint256 proposalId, uint8 option, address[] delegators)`
* `delegate(address delegatee)`
* `finalize(uint256 proposalId)`

A `ProposalParams` value bundles seven fields:

* `shareTokens`
* `snapshotBlock`
* `endsAt`
* `quorumBps`
* `options`
* `binding`
* `documentHash`

If quorum falls short, the result reads `255` (`NO_QUORUM`). [Shareholder voting](/fundamentals/shareholder-voting.md) walks through the process.

### Document anchoring: DisclosureRegistry

A document's hash goes on-chain here, and its contents stay off-chain: the registry does not hold them.

* `anchor(bytes32 kind, bytes32 hash, string uri) returns (uint256 documentId)`
* `document(uint256 documentId) returns (Document)`

## Pricing and administration

### Valuations: ValuationOracle

Each share class publishes its [Reference Valuation](/fundamentals/valuation-publishing.md) through this oracle. Because it offers the read functions of Chainlink's `AggregatorV3Interface` (`latestRoundData`, `decimals`, `description`), integrations already built for those need no changes. A fuller read is also available:

```solidity
function latestValuation()
    external view
    returns (int256 price, uint8 source, uint64 asOf, bytes32 documentHash);
```

Prices carry 8 decimals and are denominated in USD. Three values are possible for `source`:

| Value | Name                    |
| ----- | ----------------------- |
| `0`   | `PRICED_ROUND`          |
| `1`   | `INDEPENDENT_VALUATION` |
| `2`   | `WINDOW_CLEARING`       |

### Delayed execution: IssuerTimelock

Sensitive company actions execute through this timelocked multi-sig, and the issuer's signers always sit on it. Its default delays are:

| Default delay | Applies to                 |
| ------------- | -------------------------- |
| 24 hours      | Module changes             |
| 48 hours      | A change to the multiplier |
| 72 hours      | Recovery                   |
| Seven days    | Token upgrade              |

A pause skips the wait entirely. [Access control](/under-the-hood/access-control.md) sets out who holds what.

For event signatures, contract by contract, go to the [Chain event catalog](/build-on-selcier/chain-events.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.selcier.xyz/build-on-selcier/smart-contracts.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
