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
| Role | keccak256 constant | Who holds it | What it allows |
|---|---|---|---|
ADMIN_ROLE | keccak256("ADMIN_ROLE") | Oracle service account | createProposal, executeProposal |
GOVERNANCE_ROLE | keccak256("GOVERNANCE_ROLE") | NCRB signers | approveProposal, rejectProposal |
ROLE_MANAGER | keccak256("ROLE_MANAGER") | Admin | Grant/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
recipientAddressfields must be non-zero. Σ percentage == 10 000(exactly 100%).- First recipient (index 0) is treated as the asset owner.
Constants
| Constant | Value | Meaning |
|---|---|---|
REQUIRED_APPROVALS | 3 | Votes needed to reach approval threshold |
REQUIRED_REJECTIONS | 3 | Votes needed to cancel a proposal |
MAX_DISTRIBUTION_RECIPIENTS | 10 | Maximum recipients per proposal |
TOTAL_BASIS_POINTS | 10 000 | Denominator for percentage calculations |
PROPOSAL_EXPIRATION | 7 days | Window 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:
executeProposaldoes not callmintitself — 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
| Event | When 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:
| Recipient | Basis points | Tokens |
|---|---|---|
| Project Owner | 9 900 | 990 000 |
| NCRB Asset Treasury | 100 | 10 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.