-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFactoringEscrow.sol
More file actions
75 lines (63 loc) · 3.58 KB
/
Copy pathFactoringEscrow.sol
File metadata and controls
75 lines (63 loc) · 3.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IIdentityRegistry} from "./interfaces/IIdentityRegistry.sol";
/// @title FactoringEscrow
/// @notice The CVA half of the 30-point axis, made structurally real rather than
/// decorative. Real Cleanverse aUSDC only transfers between A-Pass-verified
/// wallets — that restriction lives in the ASSET, not in whatever app sits on
/// top of it. `MockAToken` is a bare ERC-20 stand-in (real aUSDC's transfer
/// gate ABI isn't public, so a pool/escrow contract can't hold real aUSDC), so
/// this contract re-derives the same guarantee independently: every cash leg
/// re-checks both parties against `IdentityRegistry` immediately before moving
/// value, and reverts `NonCompliantSettlementParty` if either fails.
///
/// This is deliberate defense-in-depth: `ReceivableNote` enforces who may HOLD
/// the note (the security layer), `FactoringEscrow` independently enforces who
/// may RECEIVE the money (the asset layer) — exactly mirroring how real
/// aUSDC's own transfer-restriction is separate from any compliance logic an
/// app builds on top of it. Even bypassing the note's gate hypothetically, the
/// money leg alone still blocks a non-verified party.
contract FactoringEscrow is Ownable {
using SafeERC20 for IERC20;
IIdentityRegistry public immutable identityRegistry;
/// @notice The only contract allowed to trigger cash legs (ReceivableNote).
address public note;
event AdvanceFunded(address indexed asset, address indexed financier, address indexed supplier, uint256 amount);
event RedemptionSettled(address indexed asset, address indexed obligor, address indexed holder, uint256 amount);
event NoteChanged(address indexed note);
error NotNote();
error NonCompliantSettlementParty(address party);
modifier onlyNote() {
if (msg.sender != note) revert NotNote();
_;
}
constructor(address identityRegistryAddr) Ownable(msg.sender) {
identityRegistry = IIdentityRegistry(identityRegistryAddr);
}
function setNote(address noteAddr) external onlyOwner {
note = noteAddr;
emit NoteChanged(noteAddr);
}
/// @notice Pulls the discounted advance from the financier to the supplier.
/// Financier must have approved this contract for `amount` beforehand.
function fundAdvance(address asset, address financier, address supplier, uint256 amount) external onlyNote {
_requireCompliant(financier);
_requireCompliant(supplier);
IERC20(asset).safeTransferFrom(financier, supplier, amount);
emit AdvanceFunded(asset, financier, supplier, amount);
}
/// @notice Pulls the face value from the obligor to whoever currently holds the
/// note at maturity. Obligor must have approved this contract beforehand.
function settleRedemption(address asset, address obligor, address holder, uint256 amount) external onlyNote {
_requireCompliant(obligor);
_requireCompliant(holder);
IERC20(asset).safeTransferFrom(obligor, holder, amount);
emit RedemptionSettled(asset, obligor, holder, amount);
}
function _requireCompliant(address party) internal view {
if (!identityRegistry.isVerified(party)) revert NonCompliantSettlementParty(party);
}
}