DealManager
Primary deals and the secondary-trading venue with settlement escrow
Runs a cyberCORP's deals: primary issuance deals built on the agreement registry, and a secondary-trading venue with its own offer and settlement machinery. A deal is created from an agreement template and identified by a bytes32 agreementId.
Source:
src/DealManager.sol/ interfaceIDealManager.solPattern: UUPS proxy. The
DealManagerStorage,SecondaryTradeStorageandLexScrowStoragelibraries hold most of the logic. The deal and secondary-trade events and errors are declared in theIDealManagerStorageandISecondaryTradeStorageinterfaces so they appear in DealManager's ABI.ILexScrowStoragecontributes the sharedAgreementConditionsNotMeterror and the escrow views, and the escrow events (DealPaidAtand the others) are declared in theLexScrowStoragelibrary itself. All of them are emitted from the DealManager's address.DEPLOY_VERSION:"5". The DealManager of a company that has not upgraded reports"4.0.1"or lower; readDEPLOY_VERSION()on the instance before choosing an ABI (see Integrate from a frontend).
Primary deal lifecycle
function proposeDeal(address[] _certPrinterAddress, address _paymentToken,
uint256 _paymentAmount, bytes32 _templateId, uint256 _salt,
string[] _globalValues, address[] _parties, CertificateDetails[] _certDetails,
string[][] _partyValues, address[] conditions, bytes32 secretHash,
uint256 expiry) external
returns (bytes32 agreementId, uint256[] certIds); // onlyOwner
function proposeAndSignDeal(/* ...as above, plus */ address proposer,
bytes signature /* ... */) external
returns (bytes32 agreementId, uint256[] certIds); // onlyOwner
function proposeAndSignNewCertsDeal(uint256 salt, CyberCertData[] _certData,
bytes32 _templateId, string[] _globalValues, address[] _parties,
uint256 _paymentAmount, string[][] _partyValues, bytes signature,
CertificateDetails[] _details, address[] conditions, bytes32 secretHash,
uint256 expiry, address stableAddress) external
returns (address[] certPrinterAddress, bytes32 id, uint256[] certIds); // onlyOwner
function signDealAndPay(address signer, bytes32 agreementId, bytes signature,
string[] partyValues, bool _fillUnallocated, string name, string secret) external;
function signAndFinalizeDeal(address signer, bytes32 agreementId,
string[] partyValues, bytes signature, bool _fillUnallocated,
string name, string secret) external;
function finalizeDeal(bytes32 agreementId) external;
function voidExpiredDeal(bytes32 _agreementId, address signer, bytes signature) external;
function revokeDeal(bytes32 _agreementId, address signer, bytes signature) external;
function signToVoid(bytes32 _agreementId, address signer, bytes signature) external;
function refundVoidedDeal(bytes32 agreementId) external; // for deals voided directly in the registry
function addCondition(bytes32 agreementId, address condition) external; // onlyOwner, pending deals only
function removeConditionAt(bytes32 agreementId, uint256 index) external; // onlyOwner, pending deals only
function initialize(address _auth, address _corp, address _dealRegistry,
address _issuanceManager, address _upgradeFactory) external;proposeAndSignNewCertsDeal deploys new LET contracts (through IssuanceManager.createCertPrinter, prefixing the company name) and proposes and signs the deal in one transaction. CyberCertData carries {name, symbol, uri, securityClass, securitySeries, extension, seriesData, defaultLegend}; it is declared once in src/CyberCorpConstants.sol and shared by the deal path, the round path and the corporate factories. A v4 DealManager takes the struct without seriesData, so the function selector differs between v4 and v5.
proposeDeal creates the registry agreement with the DealManager as its finalizer, so only the DealManager can finalize it and the registry accepts a relayed signature only when the DealManager submits it. Parties sign the registry's SignatureData with signer set to their own address (see CyberAgreementRegistry).
How primary deals work
A deal references an agreement template (
_templateId) and is recorded in the CyberAgreementRegistry, passed toinitializeas_dealRegistry._partiescountersign with EIP-712 signatures;signDealAndPaycombines a party's signature with their payment.conditionsareIConditionaddresses that gate the deal. The owner canaddCondition/removeConditionAtwhile the deal is pending.expiryandsecretHashsupport timed and secret-gated deals, andvoidExpiredDealcleans up expired ones.expiry == 0means no deadline: such a deal can pay and finalize at any time and can never be voided as expired (voidExpiredDealrevertsDealNotExpired), so it unwinds only by void request (see Voiding a primary deal).finalizeDealapplies the deal's LET effects (mint, assign, endorse) through the IssuanceManager and releases the escrowed payment, less the primary platform fee (computeFee/getPlatformPayable).Escrow state lives in the shared
LexScrowStoragelibrary (see LeXscroWLite).getEscrowDetails(agreementId)andconditionCheck(agreementId)expose it.
Voiding a primary deal
Every void runs through the registry's voidContractFor. It voids the agreement once every allocated party has requested it, as soon as the proposer (party index 0) requests while still the only signer, or, for a nonzero expiry only, once that expiry has passed. The registry knows nothing of the escrow, so the DealManager entry points add the teardown: they void the company's Ledger Entry Tokens (LETs) minted into escrow at proposal and refund or close the escrow.
signToVoidforwards the request. Once the registry reports the agreement voided, it voids the escrowed LETs and settles the escrow: aPAIDescrow is refunded and aPENDINGone is markedVOIDED. Until then it only records the request.revokeDealis the same route for a deal that has not been paid. It revertsDealNotPendingunless the escrow isPENDINGand forwards the request. When that request voids the agreement (the sole-signer proposer's request, for example), it voids the escrowed LETs and marks the escrowVOIDED.Both require the caller to be the
signerthey name (CounterPartyValueMismatchotherwise).voidExpiredDealapplies only to a deal whose nonzero expiry has passed. It forwards the request, voids the LETs, and refunds or closes the escrow.Requests can also go straight to the registry, bypassing the DealManager. The escrow then lags the agreement until
refundVoidedDealsyncs it, which voids the company's LETs and refunds, for aPAIDescrow only. On aPENDINGescrowvoidAndRefundrevertsEscrowNotPaid, so an unpaid deal voided this way still needssignToVoidfrom a party that has not yet requested the void.
Secondary trading
DealManager is also the SPV-side secondary-trading venue for LETs: sell and buy offers, partial acceptances, settlement escrows, and layered compliance conditions.
postOffer, acceptOffer and cancelOffer each have a relayer overload taking (…, address forAddr, uint256 nonce, bytes sig), where sig is the user's EIP-712 authorization, so a relayer can submit on a user's behalf. The relayed voidSecondaryTradeAgreement overload has its own shape, (agreementId, signer, voidSignature, nonce, authSig): the registry void signature and the relayer-authorization signature are separate, with signer as the authorized party.
Offers.
PostOfferParamscovers both sides (OfferSide.SELL/BUY): the LET contract and token id, units, payment token and consideration, validity window, an agreement template with party values and the offeror's signature, an open endorsement signature (sell side), and the buyer's hosting mode. Offer status runsLIVE → PARTIALLY_ACCEPTED / FULLY_ACCEPTED → FINALIZED, orCANCELLED.Acceptance and settlement.
acceptOfferfills an offer fully or partially: it reserves the seller's LET units, escrows the buyer's consideration and creates a settlement agreement. Each fill is priced from the offer's running total. The fill paysoffer.consideration × (offer.unitsAccepted + params.units) / offer.units − offer.paymentAccepted, whereparams.unitsis this fill's size andoffer.unitsthe offer's total (integer division rounds down; a negative result counts as zero). Rounding error therefore does not grow with the number of fills, and the fill that exhausts the offer pays the remainder. A priced fill that rounds to zero revertsZeroConsiderationFill.finalizeSecondaryTradeAgreementsettles the trade throughIssuanceManager.secondaryTransfer: the LET never moves wallets, and legal ownership transfers through its metadata. Voiding anACCEPTEDsettlement takes both parties' void requests, or expiry.Exemption pathways. Every trade travels under an
ExemptionPathway(NONE,RULE_144,SECTION_4A7,SECTION_4A1HALF,RULE_144A,REGULATION_S). A sell offer may pin one pathway or leaveNONEso each buyer elects one at acceptance; buy offers must specify one. Only pathways the SPV has enabled can be pinned or elected.Hosting modes.
HostingMode.DIRECTdelivers the LET to the buyer.ADMINISTEREDdelivers it to an admin multisig and registers the buyer as legal owner.ADMINISTEREDwith a zeroadminMultisigrevertsMissingAdminMultisigatpostOffer(buy offers) and atacceptOffer.Void LETs. A void seller LET keeps its owner and its units, so the ownership and unit checks alone would pass it. Posting a sell offer against it, accepting an offer that settles from it, and finalizing all revert (
CertificateVoidedat post and accept, the LET contract'sVoidCertificateat finalize).
Secondary-trade configuration
Conditions are layered. Exemption-specific pathway conditions and fund-specific SPV threshold conditions are read live at post and accept and re-checked at finalization; closing conditions are evaluated at finalization. Each layer is set as a whole list. Views: getSpvThresholdConditions, getPathwayThresholdConditions, isPathwayEnabled, getClosingConditions, getMinTradeThreshold, getDefaultIntegrator, getSettlementWindow, getOffer(offerId), getSecondaryEscrow(agreementId). See Conditions.
Configuration and fees
setDealRegistry, setCorp, setIssuanceManager (all onlyOwner); issuanceManager(), getCounterPartyValues(agreementId); computeFee(size) and getPlatformPayable() for the platform fee recipient.
The DealManagerFactory prices primary deals and secondary trades separately, both in basis points of the ticket (BASIS_POINTS = 10000):
Primary (
computeFee, used byfinalizeDeal): the factory'sgetDefaultFeeRatio(). Despite its name, it returns the per-DealManager override when one is enabled (setInstanceFeeOverride) and the platform default otherwise; deployed DealManagers of every version call it by that name.Secondary (
finalizeSecondaryTradeAgreement): the factory'sgetSecondaryFeeRatio(), with its own per-DealManager override (setSecondaryInstanceFeeOverride) and default (setDefaultSecondaryFeeRatio). The integrator's share comes out of that fee.
The factory owner (MetaLeX) sets the defaults and overrides. An override with enabled = true and a zero ratio is a real 0% rate. Read the platform defaults, ignoring overrides, with getUnderlyingDefaultFeeRatio() / getUnderlyingDefaultSecondaryFeeRatio(), and one instance's override with getInstanceFeeOverride(dm) / getSecondaryInstanceFeeOverride(dm). A DealManager implementation that does not call getSecondaryFeeRatio(), which includes every v4 DealManager, charges getDefaultFeeRatio() on secondary trades as well until it is upgraded.
Events
Declared on DealManager itself: MinTradeThresholdSet, SettlementWindowSet, DealRegistrySet, CorpSet, IssuanceManagerSet and DefaultIntegratorSet (each setter event carries the new value, the old value and the caller). From the libraries, through the interfaces: DealProposed, DealFinalized (IDealManagerStorage); DealPaidAt, DealVoidedAt, DealFinalizedAt, FeeDistributed (LexScrowStorage); OfferPosted, OfferCancelled, OfferAccepted, SecondaryTradeAgreementFinalized, SecondaryTradeAgreementVoided, SecondaryFeeDistributed, SpvThresholdConditionsSet, PathwayThresholdConditionsSet, ClosingConditionsSet (ISecondaryTradeStorage).
Upgrades
_authorizeUpgrade is onlyOwner and accepts only the DealManagerFactory's current reference implementation (NotRefImplementation otherwise).
Last updated
Was this helpful?
