fix(contracts): cierra migración ink! v6 (credential) + FIX-1 V1-H7 (escrow milestone sum)
Completa la capa de contratos: los 4 templates buildables migrados a ink! 6.0.0-beta.1 (17 tests verde). El 5º (mdk-reconciliation) es design-only (DESIGN.md, sin código Rust — nada que migrar). origin-credential-mint: migración ink! v6 (Address/U256/StorageLayout). Su issue() ya era CEI-correcto (muta estado antes del transfer) — es el template que el roadmap cita como referencia. 4 tests verde (+ guard test). NOTA: FIX-4 (verify de issuer_signature on-chain) sigue pendiente — necesita el payload canónico de credencial + precompile Ed25519 (Phase 3.x), no es parte de la migración. FIX-1 V1-H7 (escrow): enforce Σ milestone.amount <= total_amount en add_milestone (previene sobre-liberación) + completion vía milestone_total + sweep terminal del residual no-asignado al depositor (deposited − milestone_total). 5 tests verde (+ test del invariante con set_caller). El sweep (transfer) se valida E2E en nodo — node-gated. 17 tests verde totales (escrow 5 + staking 4 + nft 4 + credential 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6663dbdc0f
commit
34a2041f2b
2 files changed
+97
-27
No files matched your search
@@ -54,13 +54,14 @@
|
||||
mod origin_credential_mint {
|
||||
use ink::prelude::vec::Vec;
|
||||
use ink::storage::Mapping;
|
||||
use ink::U256;
|
||||
|
||||
pub type CredentialId = u32;
|
||||
pub type DataHash = [u8; 32];
|
||||
pub type Ed25519Signature = [u8; 64];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, scale::Encode, scale::Decode)]
|
||||
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
|
||||
pub enum CredentialType {
|
||||
/// Diploma universitario, certificate, transcript canonical.
|
||||
Academic,
|
||||
@@ -75,10 +76,10 @@ mod origin_credential_mint {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, scale::Encode, scale::Decode)]
|
||||
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
|
||||
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
|
||||
pub struct Credential {
|
||||
pub issuer: AccountId,
|
||||
pub holder: AccountId,
|
||||
pub issuer: Address,
|
||||
pub holder: Address,
|
||||
pub schema_type: CredentialType,
|
||||
pub data_hash: DataHash,
|
||||
pub issuer_signature: Ed25519Signature,
|
||||
@@ -90,18 +91,18 @@ mod origin_credential_mint {
|
||||
|
||||
#[ink(storage)]
|
||||
pub struct OriginCredentialMint {
|
||||
owner: AccountId,
|
||||
treasury: AccountId,
|
||||
owner: Address,
|
||||
treasury: Address,
|
||||
issue_fee: Balance,
|
||||
|
||||
/// Whitelist canonical de issuers autorizados (Foundation manages).
|
||||
/// Sussi Ethos Oracle add/remove canonical via governance Phase 5.
|
||||
authorized_issuers: Mapping<AccountId, bool>,
|
||||
authorized_issuers: Mapping<Address, bool>,
|
||||
|
||||
total_credentials: u32,
|
||||
credentials: Mapping<CredentialId, Credential>,
|
||||
holder_credentials: Mapping<AccountId, Vec<CredentialId>>,
|
||||
issuer_credentials: Mapping<AccountId, Vec<CredentialId>>,
|
||||
holder_credentials: Mapping<Address, Vec<CredentialId>>,
|
||||
issuer_credentials: Mapping<Address, Vec<CredentialId>>,
|
||||
}
|
||||
|
||||
#[ink(event)]
|
||||
@@ -109,9 +110,9 @@ mod origin_credential_mint {
|
||||
#[ink(topic)]
|
||||
credential_id: CredentialId,
|
||||
#[ink(topic)]
|
||||
issuer: AccountId,
|
||||
issuer: Address,
|
||||
#[ink(topic)]
|
||||
holder: AccountId,
|
||||
holder: Address,
|
||||
schema_type: CredentialType,
|
||||
data_hash: DataHash,
|
||||
expires_at: Option<BlockNumber>,
|
||||
@@ -122,14 +123,14 @@ mod origin_credential_mint {
|
||||
#[ink(topic)]
|
||||
credential_id: CredentialId,
|
||||
#[ink(topic)]
|
||||
issuer: AccountId,
|
||||
issuer: Address,
|
||||
revoked_at: BlockNumber,
|
||||
}
|
||||
|
||||
#[ink(event)]
|
||||
pub struct IssuerAuthorized {
|
||||
#[ink(topic)]
|
||||
issuer: AccountId,
|
||||
issuer: Address,
|
||||
authorized: bool,
|
||||
}
|
||||
|
||||
@@ -150,10 +151,21 @@ mod origin_credential_mint {
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// Convierte un valor `U256` del entorno pallet-revive a `Balance` (u128)
|
||||
/// de forma saturante. ⚠️ Ver nota de correctitud económica native↔eth en
|
||||
/// `origin-escrow` — pendiente validación E2E en nodo pallet-revive (VPS).
|
||||
fn u256_to_balance(v: U256) -> Balance {
|
||||
if v > U256::from(Balance::MAX) {
|
||||
Balance::MAX
|
||||
} else {
|
||||
v.as_u128()
|
||||
}
|
||||
}
|
||||
|
||||
impl OriginCredentialMint {
|
||||
#[ink(constructor)]
|
||||
pub fn new(
|
||||
treasury: AccountId,
|
||||
treasury: Address,
|
||||
issue_fee: Balance,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -171,10 +183,10 @@ mod origin_credential_mint {
|
||||
// ── Reads (Inmutable #37 free perpetua) ──────────────────
|
||||
|
||||
#[ink(message)]
|
||||
pub fn owner(&self) -> AccountId { self.owner }
|
||||
pub fn owner(&self) -> Address { self.owner }
|
||||
|
||||
#[ink(message)]
|
||||
pub fn treasury(&self) -> AccountId { self.treasury }
|
||||
pub fn treasury(&self) -> Address { self.treasury }
|
||||
|
||||
#[ink(message)]
|
||||
pub fn issue_fee(&self) -> Balance { self.issue_fee }
|
||||
@@ -183,7 +195,7 @@ mod origin_credential_mint {
|
||||
pub fn total_credentials(&self) -> u32 { self.total_credentials }
|
||||
|
||||
#[ink(message)]
|
||||
pub fn is_issuer_authorized(&self, issuer: AccountId) -> bool {
|
||||
pub fn is_issuer_authorized(&self, issuer: Address) -> bool {
|
||||
self.authorized_issuers.get(issuer).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -193,12 +205,12 @@ mod origin_credential_mint {
|
||||
}
|
||||
|
||||
#[ink(message)]
|
||||
pub fn credentials_of_holder(&self, holder: AccountId) -> Vec<CredentialId> {
|
||||
pub fn credentials_of_holder(&self, holder: Address) -> Vec<CredentialId> {
|
||||
self.holder_credentials.get(holder).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[ink(message)]
|
||||
pub fn credentials_of_issuer(&self, issuer: AccountId) -> Vec<CredentialId> {
|
||||
pub fn credentials_of_issuer(&self, issuer: Address) -> Vec<CredentialId> {
|
||||
self.issuer_credentials.get(issuer).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -223,7 +235,7 @@ mod origin_credential_mint {
|
||||
/// Authorize issuer canonical (owner-only — Foundation manages whitelist).
|
||||
/// Future Phase 5 governance vote requirement canonical.
|
||||
#[ink(message)]
|
||||
pub fn authorize_issuer(&mut self, issuer: AccountId, authorized: bool) -> Result<()> {
|
||||
pub fn authorize_issuer(&mut self, issuer: Address, authorized: bool) -> Result<()> {
|
||||
if self.env().caller() != self.owner {
|
||||
return Err(Error::UnauthorizedCaller);
|
||||
}
|
||||
@@ -242,7 +254,7 @@ mod origin_credential_mint {
|
||||
#[ink(message, payable)]
|
||||
pub fn issue(
|
||||
&mut self,
|
||||
holder: AccountId,
|
||||
holder: Address,
|
||||
schema_type: CredentialType,
|
||||
data_hash: DataHash,
|
||||
issuer_signature: Ed25519Signature,
|
||||
@@ -252,7 +264,7 @@ mod origin_credential_mint {
|
||||
if !self.is_issuer_authorized(issuer) {
|
||||
return Err(Error::UnauthorizedIssuer);
|
||||
}
|
||||
if self.env().transferred_value() < self.issue_fee {
|
||||
if u256_to_balance(self.env().transferred_value()) < self.issue_fee {
|
||||
return Err(Error::InsufficientPayment);
|
||||
}
|
||||
if data_hash == [0u8; 32] {
|
||||
@@ -334,7 +346,7 @@ mod origin_credential_mint {
|
||||
#[ink::test]
|
||||
fn constructor_canonical() {
|
||||
let contract = OriginCredentialMint::new(
|
||||
AccountId::from([1u8; 32]),
|
||||
Address::from([1u8; 20]),
|
||||
1_000_000_000_000_000_000, // 1 OGC issue fee
|
||||
);
|
||||
assert_eq!(contract.total_credentials(), 0);
|
||||
@@ -344,10 +356,25 @@ mod origin_credential_mint {
|
||||
#[ink::test]
|
||||
fn unauthorized_issuer_rejected() {
|
||||
let contract = OriginCredentialMint::new(
|
||||
AccountId::from([1u8; 32]),
|
||||
Address::from([1u8; 20]),
|
||||
1_000_000_000_000_000_000,
|
||||
);
|
||||
assert!(!contract.is_issuer_authorized(AccountId::from([99u8; 32])));
|
||||
assert!(!contract.is_issuer_authorized(Address::from([99u8; 20])));
|
||||
}
|
||||
|
||||
/// FIX-1 — guarda: issue desde caller no autorizado + verify/revoke sobre
|
||||
/// credencial inexistente cortan en el check ANTES de cualquier efecto o
|
||||
/// transfer. (issue() ya es CEI-correcto: muta estado antes del transfer.)
|
||||
#[ink::test]
|
||||
fn guards_block_before_state_change() {
|
||||
let mut c = OriginCredentialMint::new(Address::from([1u8; 20]), 1_000_000_000_000_000_000);
|
||||
assert_eq!(
|
||||
c.issue(Address::from([2u8; 20]), CredentialType::Academic, [7u8; 32], [0u8; 64], None),
|
||||
Err(Error::UnauthorizedIssuer)
|
||||
);
|
||||
assert_eq!(c.verify(999), Err(Error::CredentialNotFound));
|
||||
assert_eq!(c.revoke(999), Err(Error::CredentialNotFound));
|
||||
assert_eq!(c.total_credentials(), 0);
|
||||
}
|
||||
|
||||
#[ink::test]
|
||||
|
||||
Reference in new issue
Block a user