Skip to main content

MultiSigGovernance

MultiSigGovernance.sol controls RWA token minting. The oracle creates a mint proposal after quality assessment; NCRB governance signers vote 3-of-5 to approve or reject it; the oracle then executes approved proposals by calling RWAToken.mint() for each distribution recipient.

Source: contracts/core/MultiSigGovernance.sol.


Roles

Rolekeccak256 constantWho holds itWhat it allows
ADMIN_ROLEkeccak256("ADMIN_ROLE")Oracle service accountcreateProposal, executeProposal
GOVERNANCE_ROLEkeccak256("GOVERNANCE_ROLE")NCRB signersapproveProposal, rejectProposal
ROLE_MANAGERkeccak256("ROLE_MANAGER")AdminGrant/revoke GOVERNANCE_ROLE

Structs

MintProposal

struct MintProposal {
string certificateSerial; // Certificate this mint is backed by
address tokenContract; // RWAToken contract to mint on
uint256 amount; // Total tokens to mint across all recipients
uint256 proposedAt; // Unix timestamp of proposal creation
uint256 approvedAt; // Timestamp when 3rd approval was received (0 until then)
bool isExecuted; // True once oracle has minted tokens
bool isRejected; // True once 3 rejections received or proposal expired
uint8 approvalCount; // Cumulative approvals
uint8 rejectionCount; // Cumulative rejections
}

DistributionRecipient

struct DistributionRecipient {
address recipientAddress; // Wallet to receive tokens
uint16 percentage; // Share in basis points (10 000 = 100%)
string label; // e.g. "Project Owner", "NCRB Asset Treasury"
}

Distribution constraints enforced in _validateDistribution:

  • At least one recipient, maximum 10.
  • All recipientAddress fields must be non-zero.
  • Σ percentage == 10 000 (exactly 100%).
  • First recipient (index 0) is treated as the asset owner.

Constants

ConstantValueMeaning
REQUIRED_APPROVALS3Votes needed to reach approval threshold
REQUIRED_REJECTIONS3Votes needed to cancel a proposal
MAX_DISTRIBUTION_RECIPIENTS10Maximum recipients per proposal
TOTAL_BASIS_POINTS10 000Denominator for percentage calculations
PROPOSAL_EXPIRATION7 daysWindow during which signers can vote

Key Functions

createProposal

function createProposal(
string memory _certificateSerial,
address _tokenContract,
uint256 _amount,
DistributionRecipient[] memory _distribution
) external onlyRole(ADMIN_ROLE) nonReentrant returns (uint256 proposalId)

Called by the oracle after quality assessment. Validates the distribution, stores the proposal and distribution array, increments proposalCount, and emits ProposalCreated. Returns the new proposalId.

The distribution is read from assetMetadata IPFS JSON field distributionRecipients. The oracle reads this at proposal creation time via buildDistribution() in ncrb-oracles. If not present, the oracle falls back to a default split.


approveProposal

function approveProposal(uint256 _proposalId)
external onlyRole(GOVERNANCE_ROLE) nonReentrant

Casts one approval vote. Requirements:

  • Not already executed or rejected.
  • This signer has not already approved or rejected.
  • Proposal has not expired (proposedAt + 7 days).

When approvalCount >= 3, approvedAt is set. Emits ProposalApproved.


rejectProposal

function rejectProposal(uint256 _proposalId)
external onlyRole(GOVERNANCE_ROLE) nonReentrant

Casts one rejection vote. Same guard conditions as approveProposal. When rejectionCount >= 3, isRejected = true and ProposalCancelled is emitted.

A signer cannot both approve and reject the same proposal.


executeProposal

function executeProposal(uint256 _proposalId)
external onlyRole(ADMIN_ROLE) nonReentrant

Called by the oracle once 3 approvals are in. Marks isExecuted = true and emits ProposalExecuted. The oracle then calls RWAToken.mint() for each distribution recipient proportional to their basis points, and AssetRegistry.allocateVolume() to deduct from the certificate's available volume.

Note: executeProposal does not call mint itself — the actual minting is done off-chain by the oracle immediately after this call succeeds.


expireProposal

function expireProposal(uint256 _proposalId) external

Anyone can call this on a proposal past its 7-day window. Sets isRejected = true and emits ProposalExpired. No role required.


getProposal / getProposalDistribution

function getProposal(uint256 _proposalId) external view returns (MintProposal memory)
function getProposalDistribution(uint256 _proposalId) external view returns (DistributionRecipient[] memory)

Returns the proposal struct and its distribution array respectively.


isProposalApproved

function isProposalApproved(uint256 _proposalId) external view returns (bool)

Returns true when the proposal has ≥3 approvals, is not yet executed or rejected, and has not expired.


Events

EventWhen emitted
ProposalCreated(proposalId, certificateSerial, tokenContract, amount, distributionCount)createProposal
ProposalApproved(proposalId, approver, currentApprovals)Each approval vote
ProposalRejected(proposalId, rejecter, currentRejections)Each rejection vote
ProposalExecuted(proposalId, executedAt)executeProposal
ProposalCancelled(proposalId, cancelledAt)3rd rejection received
ProposalExpired(proposalId, expiredAt)expireProposal

Proposal Lifecycle

Oracle creates proposal


PENDING ◄──── Signers vote approve / reject (7-day window)

┌────┴────────────────────────────────┐
│ ≥3 approvals ≥3 rejections or expired
▼ ▼
APPROVED REJECTED/EXPIRED


Oracle calls executeProposal()


EXECUTED → Oracle calls RWAToken.mint() per recipient

Distribution Example

A proposal for 1 000 000 tokens with the default 99%/1% split:

RecipientBasis pointsTokens
Project Owner9 900990 000
NCRB Asset Treasury10010 000

The oracle reads the distributionRecipients array from the certificate's IPFS metadata and passes it verbatim to createProposal. If that field is absent, the oracle uses its hardcoded fallback split.


UUPS Upgrade

_authorizeUpgrade requires ADMIN_ROLE.