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
@@ -128,6 +128,9 @@ mod origin_escrow {
|
||||
|
||||
// ── Milestones canonical (partial releases) ─────────────
|
||||
milestone_count: u32,
|
||||
/// FIX-1 (V1-H7): Σ de los milestone.amount asignados. Invariante:
|
||||
/// milestone_total ≤ total_amount (enforced en add_milestone).
|
||||
milestone_total: Balance,
|
||||
milestones: Mapping<MilestoneId, Milestone>,
|
||||
}
|
||||
|
||||
@@ -257,6 +260,7 @@ mod origin_escrow {
|
||||
dispute_raised_by: None,
|
||||
last_release_block: None,
|
||||
milestone_count: 0,
|
||||
milestone_total: 0,
|
||||
milestones: Mapping::default(),
|
||||
})
|
||||
}
|
||||
@@ -335,6 +339,12 @@ mod origin_escrow {
|
||||
if self.env().caller() != self.depositor {
|
||||
return Err(Error::UnauthorizedCaller);
|
||||
}
|
||||
// FIX-1 (V1-H7): Σ milestone.amount no puede exceder total_amount
|
||||
// (previene sobre-liberación: releasear más de lo depositado).
|
||||
let new_total = self.milestone_total.saturating_add(amount);
|
||||
if new_total > self.total_amount {
|
||||
return Err(Error::InvalidConfiguration);
|
||||
}
|
||||
let id = self.milestone_count;
|
||||
self.milestones.insert(id, &Milestone {
|
||||
amount,
|
||||
@@ -344,6 +354,7 @@ mod origin_escrow {
|
||||
completed_at: None,
|
||||
});
|
||||
self.milestone_count = self.milestone_count.saturating_add(1);
|
||||
self.milestone_total = new_total;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
@@ -386,7 +397,16 @@ mod origin_escrow {
|
||||
self.milestones.insert(milestone_id, &milestone);
|
||||
self.released_amount = self.released_amount.saturating_add(milestone.amount);
|
||||
self.last_release_block = Some(now);
|
||||
let completed = self.released_amount >= self.total_amount;
|
||||
// FIX-1 (V1-H7): completa cuando TODOS los milestones asignados se
|
||||
// liberaron (Σ = milestone_total, garantizado ≤ total_amount por
|
||||
// add_milestone). El residual no-asignado (deposited − milestone_total,
|
||||
// p.ej. sobre-depósito) se barre al depositor al completar (sweep terminal).
|
||||
let completed = self.milestone_total > 0 && self.released_amount >= self.milestone_total;
|
||||
let sweep = if completed {
|
||||
self.deposited_amount.saturating_sub(self.milestone_total)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if completed {
|
||||
self.state = EscrowState::Completed;
|
||||
}
|
||||
@@ -398,6 +418,11 @@ mod origin_escrow {
|
||||
self.env()
|
||||
.transfer(self.treasury, U256::from(fee))
|
||||
.map_err(|_| Error::TransferFailed)?;
|
||||
if sweep > 0 {
|
||||
self.env()
|
||||
.transfer(self.depositor, U256::from(sweep))
|
||||
.map_err(|_| Error::TransferFailed)?;
|
||||
}
|
||||
|
||||
// ── EVENTS ──
|
||||
self.env().emit_event(MilestoneCompleted {
|
||||
@@ -407,7 +432,7 @@ mod origin_escrow {
|
||||
if completed {
|
||||
self.env().emit_event(EscrowCompleted {
|
||||
total_released: self.released_amount,
|
||||
total_refunded: 0,
|
||||
total_refunded: sweep,
|
||||
treasury_fee: fee,
|
||||
});
|
||||
}
|
||||
@@ -604,5 +629,23 @@ mod origin_escrow {
|
||||
// El estado permanece Created — no hubo mutación ni transfer.
|
||||
assert_eq!(escrow.state(), EscrowState::Created);
|
||||
}
|
||||
|
||||
/// FIX-1 (V1-H7): add_milestone enforce Σ milestone.amount ≤ total_amount
|
||||
/// → previene sobre-liberación. (El sweep terminal del residual se ejercita
|
||||
/// E2E con transfers — node-gated, ver nota arriba.)
|
||||
#[ink::test]
|
||||
fn milestone_total_cannot_exceed_total_amount() {
|
||||
let mut e = mk(); // depositor = [1;20], total_amount = 1 OGC (1e18)
|
||||
ink::env::test::set_caller(Address::from([1u8; 20])); // caller = depositor
|
||||
// 0.6 OGC: dentro del total → ok
|
||||
assert!(e.add_milestone(600_000_000_000_000_000, 100, [1u8; 32]).is_ok());
|
||||
// +0.6 OGC: Σ = 1.2 OGC > total → rechazado
|
||||
assert_eq!(
|
||||
e.add_milestone(600_000_000_000_000_000, 100, [1u8; 32]),
|
||||
Err(Error::InvalidConfiguration)
|
||||
);
|
||||
// +0.4 OGC: Σ = 1.0 OGC == total → ok (boundary)
|
||||
assert!(e.add_milestone(400_000_000_000_000_000, 100, [1u8; 32]).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user