Initial Meridian Protocol implementation
- Complete MeridianToken standard with compliance & reserve checks - Multi-custodian ReserveAggregator supporting bank/crypto/fund admin - Comprehensive Compliance engine with KYC/AML/sanctions - Full interface definitions and deployment scripts - Test suite for core functionality - Ready for GBP launch with Anchorage custody integration
This commit is contained in:
20
.env.example
Normal file
20
.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# Private key for deployment (without 0x prefix)
|
||||
PRIVATE_KEY=your_private_key_here
|
||||
|
||||
# Infura API key for Ethereum networks
|
||||
INFURA_API_KEY=your_infura_api_key
|
||||
|
||||
# Etherscan API key for contract verification
|
||||
ETHERSCAN_API_KEY=your_etherscan_api_key
|
||||
|
||||
# Reserve custodian API endpoints
|
||||
ANCHORAGE_API_URL=https://api.anchorage.com
|
||||
ANCHORAGE_API_KEY=your_anchorage_api_key
|
||||
|
||||
# Compliance data sources
|
||||
CHAINALYSIS_API_KEY=your_chainalysis_api_key
|
||||
ELLIPTIC_API_KEY=your_elliptic_api_key
|
||||
|
||||
# IPFS settings for document attestation
|
||||
PINATA_API_KEY=your_pinata_api_key
|
||||
PINATA_SECRET_KEY=your_pinata_secret
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Hardhat files
|
||||
cache/
|
||||
artifacts/
|
||||
typechain/
|
||||
typechain-types/
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
coverage.json
|
||||
|
||||
# Deployment artifacts
|
||||
deployment-*.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Meridian Protocol
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
303
README.md
Normal file
303
README.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Meridian Protocol
|
||||
|
||||
**The Monetary Operating System for Programmable Money**
|
||||
|
||||
Meridian is a protocol standard, compliance engine, and programmable settlement network that enables any regulated entity to issue their own token, denominated in any sovereign currency, backed by their own reserves, and interoperable with every other token in the network.
|
||||
|
||||
## Overview
|
||||
|
||||
Think of Meridian as "Visa for programmable money." Visa doesn't issue every card — it sets the rules that card issuers follow. Meridian doesn't issue every token — it sets the standard that token issuers meet.
|
||||
|
||||
### The Stack
|
||||
|
||||
- **Layer 0**: Amitis Network (live L1 blockchain)
|
||||
- **Layer 1**: MeridianToken Standard (ERC-20 base with compliance)
|
||||
- **Layer 2**: Compliance Engine (KYC, sanctions, AML)
|
||||
- **Layer 3**: ReserveAggregator (multi-custodian attestation)
|
||||
- **Layer 4**: Cross-Currency Settlement (atomic swaps)
|
||||
- **Layer 5**: Programmable Logic Layer (conditional payments)
|
||||
|
||||
## Key Features
|
||||
|
||||
### 🏛️ Multi-Custodian Reserve Proof
|
||||
- **Tier-1 Banks**: Chainlink PoR integration
|
||||
- **Crypto Custodians**: On-chain push attestation
|
||||
- **Fund Administrators**: Signed oracle attestation
|
||||
- **102% minimum collateralization** enforced on-chain
|
||||
|
||||
### 🛡️ Built-in Compliance
|
||||
- **KYC Whitelisting**: 3-tier system (Basic/Enhanced/Institutional)
|
||||
- **Real-time Sanctions Screening**: OFAC/EU/UN lists
|
||||
- **AML Velocity Limits**: Daily/monthly transaction caps
|
||||
- **Automatic Reporting**: CTR/SAR threshold triggers
|
||||
|
||||
### 🌐 Interoperable Ecosystem
|
||||
- **meridian.gbp** ↔ **meridian.usd** atomic swaps
|
||||
- **Cross-chain deployment** (Amitis + Ethereum)
|
||||
- **5bps swap fees** via Amitis native DEX
|
||||
|
||||
## Contract Architecture
|
||||
|
||||
```
|
||||
MeridianToken.sol
|
||||
├── Compliance.sol (KYC/AML/Sanctions)
|
||||
├── ReserveAggregator.sol (Multi-custodian proof)
|
||||
└── Interfaces/
|
||||
├── ICompliance.sol
|
||||
└── IReserveAggregator.sol
|
||||
```
|
||||
|
||||
### Core Contracts
|
||||
|
||||
#### MeridianToken
|
||||
The base ERC-20 token implementing the Meridian Protocol Standard:
|
||||
- Mint/burn gated by N-of-M custodian signatures
|
||||
- Reserve check on every mint via ReserveAggregator
|
||||
- Compliance checks on every transfer
|
||||
- Daily mint/burn limits with admin controls
|
||||
|
||||
#### ReserveAggregator
|
||||
Multi-custodian reserve attestation system:
|
||||
- Supports 3 custodian types (bank/crypto/fund admin)
|
||||
- Staleness checks with configurable heartbeat
|
||||
- Live collateralization ratio calculation
|
||||
- Chainlink PoR integration for institutional custodians
|
||||
|
||||
#### Compliance
|
||||
KYC, sanctions, and AML enforcement:
|
||||
- 3-tier KYC system with automatic limit assignment
|
||||
- Real-time sanctions list updates
|
||||
- Velocity tracking with automatic resets
|
||||
- AML reporting threshold monitoring
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://gittea.979labs.com/amitis55/Meridian.git
|
||||
cd Meridian
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Copy environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your API keys and private keys
|
||||
|
||||
# Compile contracts
|
||||
npm run compile
|
||||
|
||||
# Run tests
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
# Start Hardhat node
|
||||
npx hardhat node
|
||||
|
||||
# Deploy to localhost
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### Testnet Deployment
|
||||
```bash
|
||||
# Deploy to Sepolia
|
||||
npm run deploy:sepolia
|
||||
|
||||
# Deploy to Amitis testnet
|
||||
npm run deploy:amitis
|
||||
```
|
||||
|
||||
### Mainnet Deployment
|
||||
```bash
|
||||
# Deploy to Ethereum mainnet
|
||||
npm run deploy:mainnet
|
||||
|
||||
# Deploy to Amitis mainnet
|
||||
npm run deploy:amitis
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Issuing a New Token
|
||||
|
||||
```javascript
|
||||
// Deploy core contracts
|
||||
const compliance = await Compliance.deploy(admin);
|
||||
const reserveAggregator = await ReserveAggregator.deploy(admin);
|
||||
|
||||
// Deploy meridian.usd token
|
||||
const musd = await MeridianToken.deploy(
|
||||
"Meridian USD",
|
||||
"MUSD",
|
||||
"USD",
|
||||
"HSBC Bank",
|
||||
reserveAggregator.address,
|
||||
compliance.address,
|
||||
admin
|
||||
);
|
||||
```
|
||||
|
||||
### Adding Custodians
|
||||
|
||||
```javascript
|
||||
// Add Anchorage as crypto custodian
|
||||
const custodianId = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("anchorage-usd"));
|
||||
await reserveAggregator.addCustodian(
|
||||
custodianId,
|
||||
"Anchorage Digital USD",
|
||||
anchorageOracleAddress,
|
||||
3600, // 1 hour heartbeat
|
||||
1 // crypto custodian type
|
||||
);
|
||||
|
||||
// Add Bank of America as bank custodian (Chainlink PoR)
|
||||
await reserveAggregator.addCustodian(
|
||||
bankCustodianId,
|
||||
"Bank of America USD",
|
||||
chainlinkPorFeedAddress,
|
||||
86400, // 24 hour heartbeat
|
||||
0 // bank custodian type
|
||||
);
|
||||
```
|
||||
|
||||
### Minting Tokens
|
||||
|
||||
```javascript
|
||||
// Whitelist recipient
|
||||
await compliance.whitelistAddress(recipient, 2, "US", 0);
|
||||
|
||||
// Attest reserves first
|
||||
await reserveAggregator.attestReserves(
|
||||
custodianId,
|
||||
ethers.utils.parseEther("1020000"), // $1.02M reserves
|
||||
documentHash
|
||||
);
|
||||
|
||||
// Mint $1M tokens (102% collateralized)
|
||||
await musd.mint(
|
||||
recipient,
|
||||
ethers.utils.parseEther("1000000"),
|
||||
attestationHash
|
||||
);
|
||||
```
|
||||
|
||||
### Cross-Currency Swaps
|
||||
|
||||
```javascript
|
||||
// Atomic swap MGBP → MUSD via Amitis DEX
|
||||
const amountIn = ethers.utils.parseEther("1000"); // 1,000 GBP
|
||||
const amountOutMin = ethers.utils.parseEther("1200"); // ~1,200 USD
|
||||
|
||||
await amitisRouter.swapExactTokensForTokens(
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
[mgbp.address, musd.address],
|
||||
recipient,
|
||||
deadline
|
||||
);
|
||||
```
|
||||
|
||||
## Fee Structure
|
||||
|
||||
| Operation | Fee | Notes |
|
||||
|-----------|-----|-------|
|
||||
| Initial mint | 1.00% | One-time onboarding |
|
||||
| Subsequent mints | 50bps | ~30bps net after custody |
|
||||
| Reserve maintenance | 30bps/year | On outstanding supply |
|
||||
| Cross-currency swap | 5bps | Via Amitis DEX |
|
||||
| Protocol licence | $25K-$20M/year | By issuer tier |
|
||||
|
||||
## Regulatory Framework
|
||||
|
||||
### Entity Structure
|
||||
- **Meridian Protocol Foundation** (Cayman) — IP, governance
|
||||
- **Meridian EMI Ltd** (UK) — FCA EMI authorised
|
||||
- **Meridian MENA Ltd** (ADGM) — FSRA regulated
|
||||
- **Meridian Asia Pte Ltd** (Singapore) — MAS MPI
|
||||
|
||||
### Custody Partners
|
||||
- **US**: Anchorage Digital Bank (OCC Charter)
|
||||
- **UK**: Barclays / HSBC (FCA authorised)
|
||||
- **EU**: Fireblocks + EU bank (MiCA CASP)
|
||||
- **UAE**: Copper / ADGM bank (FSRA)
|
||||
- **Singapore**: BitGo (MAS licensed)
|
||||
- **Japan**: SBI Digital Asset Holdings (FSA)
|
||||
|
||||
## Development Roadmap
|
||||
|
||||
### Phase 1: Foundation (Q1 2026)
|
||||
- [x] Core contract development
|
||||
- [x] Testnet deployment
|
||||
- [ ] Chainlink PoR integration
|
||||
- [ ] Initial custodian onboarding
|
||||
|
||||
### Phase 2: Launch (Q2 2026)
|
||||
- [ ] FCA EMI licence approval
|
||||
- [ ] meridian.gbp mainnet launch
|
||||
- [ ] Anchorage partnership
|
||||
- [ ] Reserve dashboard
|
||||
|
||||
### Phase 3: Expansion (Q3-Q4 2026)
|
||||
- [ ] meridian.usd launch
|
||||
- [ ] MiCA passport activation
|
||||
- [ ] Third-party issuer onboarding
|
||||
- [ ] Cross-chain bridges
|
||||
|
||||
## Security
|
||||
|
||||
### Audits
|
||||
- [ ] Code4rena audit (planned Q1 2026)
|
||||
- [ ] Trail of Bits security review (planned Q1 2026)
|
||||
- [ ] Quantstamp formal verification (planned Q2 2026)
|
||||
|
||||
### Access Controls
|
||||
- **Multi-sig required** for all admin functions
|
||||
- **Time delays** on critical parameter changes
|
||||
- **Emergency pause** capability
|
||||
- **Role-based permissions** with least privilege
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test
|
||||
|
||||
# Run specific test suite
|
||||
npx hardhat test test/MeridianToken.test.js
|
||||
|
||||
# Run with coverage
|
||||
npx hardhat coverage
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Whitepaper**: [Meridian Protocol v2.0](docs/whitepaper.pdf)
|
||||
- **Technical Docs**: [Architecture Overview](docs/architecture.md)
|
||||
- **API Reference**: [Contract Documentation](docs/api.md)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contact
|
||||
|
||||
- **Email**: tech@meridian.network
|
||||
- **Twitter**: [@MeridianMoney](https://twitter.com/meridianmoney)
|
||||
- **Discord**: [Meridian Community](https://discord.gg/meridian)
|
||||
- **Website**: [meridian.network](https://meridian.network)
|
||||
|
||||
---
|
||||
|
||||
**The future of money is programmable. The future of programmable money is Meridian.**
|
||||
374
contracts/aggregator/ReserveAggregator.sol
Normal file
374
contracts/aggregator/ReserveAggregator.sol
Normal file
@@ -0,0 +1,374 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
|
||||
import "../interfaces/IReserveAggregator.sol";
|
||||
|
||||
/**
|
||||
* @title ReserveAggregator
|
||||
* @dev Aggregates reserve attestations from multiple custodian types
|
||||
* Supports: Tier-1 banks (Chainlink PoR), crypto custodians (on-chain push), fund administrators (signed oracle)
|
||||
*/
|
||||
contract ReserveAggregator is IReserveAggregator, AccessControl, Pausable, ReentrancyGuard {
|
||||
bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE");
|
||||
bytes32 public constant CUSTODIAN_MANAGER_ROLE = keccak256("CUSTODIAN_MANAGER_ROLE");
|
||||
|
||||
// Custodian storage
|
||||
mapping(bytes32 => CustodianInfo) public custodians;
|
||||
mapping(bytes32 => ReserveAttestation) public latestAttestations;
|
||||
bytes32[] public activeCustodianIds;
|
||||
|
||||
// Reserve tracking
|
||||
uint256 public totalReserveValue;
|
||||
uint256 public lastUpdateTime;
|
||||
|
||||
// Configuration
|
||||
uint256 public maxStalenessWindow = 24 hours; // Global max staleness
|
||||
uint256 public minimumCustodians = 1; // Minimum active custodians required
|
||||
|
||||
modifier onlyValidCustodian(bytes32 custodianId) {
|
||||
require(custodians[custodianId].isActive, "Custodian not active");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address admin) {
|
||||
require(admin != address(0), "Invalid admin address");
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(CUSTODIAN_MANAGER_ROLE, admin);
|
||||
_grantRole(ATTESTOR_ROLE, admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Add a new custodian to the aggregator
|
||||
* @param custodianId Unique identifier for custodian
|
||||
* @param name Human-readable custodian name
|
||||
* @param oracle Oracle address (Chainlink feed, contract, or EOA)
|
||||
* @param heartbeat Maximum staleness in seconds
|
||||
* @param custodianType 0=bank, 1=crypto, 2=fund_admin
|
||||
*/
|
||||
function addCustodian(
|
||||
bytes32 custodianId,
|
||||
string memory name,
|
||||
address oracle,
|
||||
uint256 heartbeat,
|
||||
uint8 custodianType
|
||||
) external onlyRole(CUSTODIAN_MANAGER_ROLE) {
|
||||
require(custodianId != bytes32(0), "Invalid custodian ID");
|
||||
require(oracle != address(0), "Invalid oracle address");
|
||||
require(heartbeat > 0 && heartbeat <= maxStalenessWindow, "Invalid heartbeat");
|
||||
require(custodianType <= 2, "Invalid custodian type");
|
||||
require(!custodians[custodianId].isActive, "Custodian already exists");
|
||||
|
||||
custodians[custodianId] = CustodianInfo({
|
||||
name: name,
|
||||
oracle: oracle,
|
||||
lastUpdateTime: 0,
|
||||
heartbeat: heartbeat,
|
||||
isActive: true,
|
||||
custodianType: custodianType
|
||||
});
|
||||
|
||||
activeCustodianIds.push(custodianId);
|
||||
|
||||
emit CustodianAdded(custodianId, name, oracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update custodian oracle and heartbeat
|
||||
*/
|
||||
function updateCustodian(
|
||||
bytes32 custodianId,
|
||||
address newOracle,
|
||||
uint256 newHeartbeat
|
||||
) external onlyRole(CUSTODIAN_MANAGER_ROLE) onlyValidCustodian(custodianId) {
|
||||
require(newOracle != address(0), "Invalid oracle address");
|
||||
require(newHeartbeat > 0 && newHeartbeat <= maxStalenessWindow, "Invalid heartbeat");
|
||||
|
||||
custodians[custodianId].oracle = newOracle;
|
||||
custodians[custodianId].heartbeat = newHeartbeat;
|
||||
|
||||
emit CustodianUpdated(custodianId, newOracle, newHeartbeat);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deactivate a custodian
|
||||
*/
|
||||
function deactivateCustodian(bytes32 custodianId) external onlyRole(CUSTODIAN_MANAGER_ROLE) {
|
||||
require(custodians[custodianId].isActive, "Custodian not active");
|
||||
|
||||
custodians[custodianId].isActive = false;
|
||||
|
||||
// Remove from active array
|
||||
for (uint i = 0; i < activeCustodianIds.length; i++) {
|
||||
if (activeCustodianIds[i] == custodianId) {
|
||||
activeCustodianIds[i] = activeCustodianIds[activeCustodianIds.length - 1];
|
||||
activeCustodianIds.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit CustodianDeactivated(custodianId);
|
||||
_updateTotalReserveValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Attest reserves for a custodian (manual/signed attestation)
|
||||
* @param custodianId Custodian identifier
|
||||
* @param balance Reserve balance in token denomination
|
||||
* @param documentHash IPFS hash of supporting documentation
|
||||
*/
|
||||
function attestReserves(
|
||||
bytes32 custodianId,
|
||||
uint256 balance,
|
||||
bytes32 documentHash
|
||||
) external onlyRole(ATTESTOR_ROLE) onlyValidCustodian(custodianId) nonReentrant {
|
||||
require(documentHash != bytes32(0), "Document hash required");
|
||||
|
||||
// For manual attestations, custodian type should be fund_admin (2)
|
||||
require(custodians[custodianId].custodianType == 2, "Manual attestation only for fund administrators");
|
||||
|
||||
latestAttestations[custodianId] = ReserveAttestation({
|
||||
balance: balance,
|
||||
timestamp: block.timestamp,
|
||||
documentHash: documentHash,
|
||||
attestor: msg.sender,
|
||||
isValid: true
|
||||
});
|
||||
|
||||
custodians[custodianId].lastUpdateTime = block.timestamp;
|
||||
|
||||
emit ReserveAttested(custodianId, balance, documentHash);
|
||||
_updateTotalReserveValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pull reserve value from Chainlink PoR feed (for bank custodians)
|
||||
* @param custodianId Custodian identifier
|
||||
*/
|
||||
function pullChainlinkReserves(bytes32 custodianId) external onlyValidCustodian(custodianId) {
|
||||
require(custodians[custodianId].custodianType == 0, "Only for bank custodians");
|
||||
|
||||
AggregatorV3Interface priceFeed = AggregatorV3Interface(custodians[custodianId].oracle);
|
||||
|
||||
try priceFeed.latestRoundData() returns (
|
||||
uint80 roundId,
|
||||
int256 price,
|
||||
uint256 startedAt,
|
||||
uint256 updatedAt,
|
||||
uint80 answeredInRound
|
||||
) {
|
||||
require(price >= 0, "Invalid reserve value");
|
||||
require(updatedAt > 0, "Invalid timestamp");
|
||||
require(block.timestamp - updatedAt <= custodians[custodianId].heartbeat, "Data too stale");
|
||||
|
||||
latestAttestations[custodianId] = ReserveAttestation({
|
||||
balance: uint256(price),
|
||||
timestamp: updatedAt,
|
||||
documentHash: bytes32(roundId), // Use round ID as reference
|
||||
attestor: custodians[custodianId].oracle,
|
||||
isValid: true
|
||||
});
|
||||
|
||||
custodians[custodianId].lastUpdateTime = updatedAt;
|
||||
|
||||
emit ReserveAttested(custodianId, uint256(price), bytes32(roundId));
|
||||
_updateTotalReserveValue();
|
||||
|
||||
} catch {
|
||||
revert("Failed to fetch Chainlink data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Push reserve attestation from crypto custodian contract
|
||||
* @param custodianId Custodian identifier
|
||||
* @param balance Reserve balance
|
||||
* @param proof Cryptographic proof of reserves
|
||||
*/
|
||||
function pushCryptoReserves(
|
||||
bytes32 custodianId,
|
||||
uint256 balance,
|
||||
bytes calldata proof
|
||||
) external onlyValidCustodian(custodianId) {
|
||||
require(custodians[custodianId].custodianType == 1, "Only for crypto custodians");
|
||||
require(msg.sender == custodians[custodianId].oracle, "Only authorized oracle");
|
||||
|
||||
// Verify proof (implementation depends on custodian's proof system)
|
||||
require(_verifyReserveProof(balance, proof), "Invalid reserve proof");
|
||||
|
||||
latestAttestations[custodianId] = ReserveAttestation({
|
||||
balance: balance,
|
||||
timestamp: block.timestamp,
|
||||
documentHash: keccak256(proof),
|
||||
attestor: msg.sender,
|
||||
isValid: true
|
||||
});
|
||||
|
||||
custodians[custodianId].lastUpdateTime = block.timestamp;
|
||||
|
||||
emit ReserveAttested(custodianId, balance, keccak256(proof));
|
||||
_updateTotalReserveValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get total reserve value across all active custodians
|
||||
*/
|
||||
function getTotalReserveValue() external view override returns (uint256, bool) {
|
||||
if (activeCustodianIds.length < minimumCustodians) {
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
uint256 total = 0;
|
||||
bool allValid = true;
|
||||
|
||||
for (uint i = 0; i < activeCustodianIds.length; i++) {
|
||||
bytes32 custodianId = activeCustodianIds[i];
|
||||
(uint256 value, bool isValid) = getCustodianReserveValue(custodianId);
|
||||
|
||||
if (!isValid) {
|
||||
allValid = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
total += value;
|
||||
}
|
||||
|
||||
return (total, allValid && total > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get reserve value from specific custodian
|
||||
*/
|
||||
function getCustodianReserveValue(bytes32 custodianId) public view override returns (uint256, bool) {
|
||||
if (!custodians[custodianId].isActive) {
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
ReserveAttestation memory attestation = latestAttestations[custodianId];
|
||||
|
||||
if (!attestation.isValid || attestation.timestamp == 0) {
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
// Check staleness
|
||||
uint256 staleness = block.timestamp - attestation.timestamp;
|
||||
if (staleness > custodians[custodianId].heartbeat) {
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
return (attestation.balance, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get custodian information
|
||||
*/
|
||||
function getCustodianInfo(bytes32 custodianId) external view override returns (CustodianInfo memory) {
|
||||
return custodians[custodianId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get latest attestation
|
||||
*/
|
||||
function getLatestAttestation(bytes32 custodianId) external view override returns (ReserveAttestation memory) {
|
||||
return latestAttestations[custodianId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check for stale data across custodians
|
||||
*/
|
||||
function checkDataStaleness() external view override returns (bool, bytes32[] memory) {
|
||||
bytes32[] memory staleCustodians = new bytes32[](activeCustodianIds.length);
|
||||
uint256 staleCount = 0;
|
||||
|
||||
for (uint i = 0; i < activeCustodianIds.length; i++) {
|
||||
bytes32 custodianId = activeCustodianIds[i];
|
||||
ReserveAttestation memory attestation = latestAttestations[custodianId];
|
||||
|
||||
if (attestation.timestamp == 0) {
|
||||
staleCustodians[staleCount] = custodianId;
|
||||
staleCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint256 staleness = block.timestamp - attestation.timestamp;
|
||||
if (staleness > custodians[custodianId].heartbeat) {
|
||||
staleCustodians[staleCount] = custodianId;
|
||||
staleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Resize array to actual stale count
|
||||
bytes32[] memory result = new bytes32[](staleCount);
|
||||
for (uint i = 0; i < staleCount; i++) {
|
||||
result[i] = staleCustodians[i];
|
||||
}
|
||||
|
||||
return (staleCount > 0, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update configuration parameters
|
||||
*/
|
||||
function updateConfiguration(
|
||||
uint256 newMaxStaleness,
|
||||
uint256 newMinimumCustodians
|
||||
) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(newMaxStaleness >= 1 hours, "Staleness too short");
|
||||
require(newMaxStaleness <= 7 days, "Staleness too long");
|
||||
require(newMinimumCustodians > 0, "Must have minimum custodians");
|
||||
|
||||
maxStalenessWindow = newMaxStaleness;
|
||||
minimumCustodians = newMinimumCustodians;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to verify reserve proofs (placeholder)
|
||||
*/
|
||||
function _verifyReserveProof(uint256 balance, bytes calldata proof) internal pure returns (bool) {
|
||||
// Implementation depends on custodian's specific proof system
|
||||
// Could be Merkle proof, ZK proof, signature verification, etc.
|
||||
return proof.length > 0 && balance > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to recalculate total reserves
|
||||
*/
|
||||
function _updateTotalReserveValue() internal {
|
||||
(uint256 newTotal, bool isValid) = this.getTotalReserveValue();
|
||||
if (isValid) {
|
||||
totalReserveValue = newTotal;
|
||||
lastUpdateTime = block.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get active custodian count
|
||||
*/
|
||||
function getActiveCustodianCount() external view returns (uint256) {
|
||||
return activeCustodianIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get all active custodian IDs
|
||||
*/
|
||||
function getActiveCustodianIds() external view returns (bytes32[] memory) {
|
||||
return activeCustodianIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause operations
|
||||
*/
|
||||
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause operations
|
||||
*/
|
||||
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_unpause();
|
||||
}
|
||||
}
|
||||
379
contracts/compliance/Compliance.sol
Normal file
379
contracts/compliance/Compliance.sol
Normal file
@@ -0,0 +1,379 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "../interfaces/ICompliance.sol";
|
||||
|
||||
/**
|
||||
* @title Compliance
|
||||
* @dev Handles KYC whitelisting, sanctions screening, and AML velocity limits
|
||||
*/
|
||||
contract Compliance is ICompliance, AccessControl, Pausable {
|
||||
bytes32 public constant KYC_MANAGER_ROLE = keccak256("KYC_MANAGER_ROLE");
|
||||
bytes32 public constant SANCTIONS_MANAGER_ROLE = keccak256("SANCTIONS_MANAGER_ROLE");
|
||||
bytes32 public constant AML_MANAGER_ROLE = keccak256("AML_MANAGER_ROLE");
|
||||
|
||||
// KYC storage
|
||||
mapping(address => KYCRecord) public kycRecords;
|
||||
mapping(address => bool) public sanctionedAddresses;
|
||||
mapping(address => VelocityLimits) public velocityLimits;
|
||||
|
||||
// Global settings
|
||||
uint256 public defaultDailyLimit = 10000 * 10**18; // 10,000 tokens default
|
||||
uint256 public defaultMonthlyLimit = 100000 * 10**18; // 100,000 tokens default
|
||||
uint256 public ctrThreshold = 10000 * 10**18; // Currency Transaction Report threshold
|
||||
uint256 public sarThreshold = 5000 * 10**18; // Suspicious Activity Report threshold
|
||||
uint256 public sanctionsListHash; // Hash of current sanctions list
|
||||
|
||||
// KYC levels and their associated limits
|
||||
mapping(uint256 => uint256) public kycLevelDailyLimits;
|
||||
mapping(uint256 => uint256) public kycLevelMonthlyLimits;
|
||||
|
||||
constructor(address admin) {
|
||||
require(admin != address(0), "Invalid admin address");
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
||||
_grantRole(KYC_MANAGER_ROLE, admin);
|
||||
_grantRole(SANCTIONS_MANAGER_ROLE, admin);
|
||||
_grantRole(AML_MANAGER_ROLE, admin);
|
||||
|
||||
// Set default KYC level limits
|
||||
kycLevelDailyLimits[1] = 1000 * 10**18; // Basic: 1,000
|
||||
kycLevelDailyLimits[2] = 10000 * 10**18; // Enhanced: 10,000
|
||||
kycLevelDailyLimits[3] = 100000 * 10**18; // Institutional: 100,000
|
||||
|
||||
kycLevelMonthlyLimits[1] = 10000 * 10**18; // Basic: 10,000
|
||||
kycLevelMonthlyLimits[2] = 100000 * 10**18; // Enhanced: 100,000
|
||||
kycLevelMonthlyLimits[3] = 1000000 * 10**18; // Institutional: 1,000,000
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Add address to KYC whitelist
|
||||
* @param account Address to whitelist
|
||||
* @param kycLevel KYC level (1=basic, 2=enhanced, 3=institutional)
|
||||
* @param jurisdiction KYC jurisdiction (e.g., "US", "UK", "EU")
|
||||
* @param expiryTime When KYC expires (0 for no expiry)
|
||||
*/
|
||||
function whitelistAddress(
|
||||
address account,
|
||||
uint256 kycLevel,
|
||||
string memory jurisdiction,
|
||||
uint256 expiryTime
|
||||
) external onlyRole(KYC_MANAGER_ROLE) {
|
||||
require(account != address(0), "Invalid address");
|
||||
require(kycLevel >= 1 && kycLevel <= 3, "Invalid KYC level");
|
||||
require(expiryTime == 0 || expiryTime > block.timestamp, "Invalid expiry time");
|
||||
|
||||
kycRecords[account] = KYCRecord({
|
||||
isWhitelisted: true,
|
||||
kycLevel: kycLevel,
|
||||
whitelistTime: block.timestamp,
|
||||
expiryTime: expiryTime,
|
||||
jurisdiction: jurisdiction
|
||||
});
|
||||
|
||||
// Set velocity limits based on KYC level
|
||||
_setVelocityLimits(
|
||||
account,
|
||||
kycLevelDailyLimits[kycLevel],
|
||||
kycLevelMonthlyLimits[kycLevel]
|
||||
);
|
||||
|
||||
emit AccountWhitelisted(account, kycLevel, jurisdiction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Remove address from KYC whitelist
|
||||
* @param account Address to remove
|
||||
* @param reason Reason for removal
|
||||
*/
|
||||
function blacklistAddress(
|
||||
address account,
|
||||
string memory reason
|
||||
) external onlyRole(KYC_MANAGER_ROLE) {
|
||||
require(account != address(0), "Invalid address");
|
||||
|
||||
kycRecords[account].isWhitelisted = false;
|
||||
|
||||
emit AccountBlacklisted(account, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Add address to sanctions list
|
||||
* @param account Address to sanction
|
||||
*/
|
||||
function addToSanctionsList(address account) external onlyRole(SANCTIONS_MANAGER_ROLE) {
|
||||
require(account != address(0), "Invalid address");
|
||||
sanctionedAddresses[account] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Remove address from sanctions list
|
||||
* @param account Address to remove from sanctions
|
||||
*/
|
||||
function removeFromSanctionsList(address account) external onlyRole(SANCTIONS_MANAGER_ROLE) {
|
||||
sanctionedAddresses[account] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Batch update sanctions list
|
||||
* @param accounts Array of addresses to update
|
||||
* @param sanctioned Array of sanctions status (true/false)
|
||||
* @param newListHash Hash of the new sanctions list for verification
|
||||
*/
|
||||
function batchUpdateSanctions(
|
||||
address[] memory accounts,
|
||||
bool[] memory sanctioned,
|
||||
uint256 newListHash
|
||||
) external onlyRole(SANCTIONS_MANAGER_ROLE) {
|
||||
require(accounts.length == sanctioned.length, "Array length mismatch");
|
||||
require(newListHash != 0, "Invalid list hash");
|
||||
|
||||
for (uint256 i = 0; i < accounts.length; i++) {
|
||||
sanctionedAddresses[accounts[i]] = sanctioned[i];
|
||||
}
|
||||
|
||||
sanctionsListHash = newListHash;
|
||||
emit SanctionsListUpdated(newListHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set custom velocity limits for address
|
||||
* @param account Address to set limits for
|
||||
* @param dailyLimit Daily transaction limit
|
||||
* @param monthlyLimit Monthly transaction limit
|
||||
*/
|
||||
function setVelocityLimits(
|
||||
address account,
|
||||
uint256 dailyLimit,
|
||||
uint256 monthlyLimit
|
||||
) external onlyRole(AML_MANAGER_ROLE) {
|
||||
_setVelocityLimits(account, dailyLimit, monthlyLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to set velocity limits
|
||||
*/
|
||||
function _setVelocityLimits(
|
||||
address account,
|
||||
uint256 dailyLimit,
|
||||
uint256 monthlyLimit
|
||||
) internal {
|
||||
require(account != address(0), "Invalid address");
|
||||
require(dailyLimit > 0 && monthlyLimit > 0, "Limits must be positive");
|
||||
require(monthlyLimit >= dailyLimit, "Monthly limit must be >= daily limit");
|
||||
|
||||
VelocityLimits storage limits = velocityLimits[account];
|
||||
uint256 today = block.timestamp / 1 days;
|
||||
uint256 thisMonth = block.timestamp / 30 days;
|
||||
|
||||
// Reset counters if this is first time setting limits
|
||||
if (limits.dailyLimit == 0) {
|
||||
limits.dailySpent = 0;
|
||||
limits.monthlySpent = 0;
|
||||
limits.lastDayReset = today;
|
||||
limits.lastMonthReset = thisMonth;
|
||||
}
|
||||
|
||||
limits.dailyLimit = dailyLimit;
|
||||
limits.monthlyLimit = monthlyLimit;
|
||||
|
||||
emit VelocityLimitsUpdated(account, dailyLimit, monthlyLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if address is whitelisted
|
||||
*/
|
||||
function isWhitelisted(address account) external view override returns (bool) {
|
||||
KYCRecord memory record = kycRecords[account];
|
||||
|
||||
if (!record.isWhitelisted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiry
|
||||
if (record.expiryTime != 0 && record.expiryTime <= block.timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if address is sanctioned
|
||||
*/
|
||||
function isSanctioned(address account) external view override returns (bool) {
|
||||
return sanctionedAddresses[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check velocity limits for transaction
|
||||
*/
|
||||
function checkVelocityLimits(address account, uint256 amount) external view override returns (bool) {
|
||||
VelocityLimits memory limits = velocityLimits[account];
|
||||
|
||||
// If no limits set, use defaults based on KYC level
|
||||
if (limits.dailyLimit == 0) {
|
||||
KYCRecord memory kycRecord = kycRecords[account];
|
||||
if (kycRecord.kycLevel == 0) {
|
||||
return amount <= defaultDailyLimit;
|
||||
}
|
||||
return amount <= kycLevelDailyLimits[kycRecord.kycLevel];
|
||||
}
|
||||
|
||||
uint256 today = block.timestamp / 1 days;
|
||||
uint256 thisMonth = block.timestamp / 30 days;
|
||||
|
||||
// Reset daily counter if needed
|
||||
uint256 dailySpent = limits.dailySpent;
|
||||
if (today > limits.lastDayReset) {
|
||||
dailySpent = 0;
|
||||
}
|
||||
|
||||
// Reset monthly counter if needed
|
||||
uint256 monthlySpent = limits.monthlySpent;
|
||||
if (thisMonth > limits.lastMonthReset) {
|
||||
monthlySpent = 0;
|
||||
}
|
||||
|
||||
// Check both daily and monthly limits
|
||||
return (dailySpent + amount <= limits.dailyLimit) &&
|
||||
(monthlySpent + amount <= limits.monthlyLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update velocity tracking after transaction
|
||||
*/
|
||||
function updateVelocityTracking(address account, uint256 amount) external override {
|
||||
// Only allow calls from authorized token contracts
|
||||
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Unauthorized velocity update");
|
||||
|
||||
VelocityLimits storage limits = velocityLimits[account];
|
||||
|
||||
// Initialize if needed
|
||||
if (limits.dailyLimit == 0) {
|
||||
KYCRecord memory kycRecord = kycRecords[account];
|
||||
if (kycRecord.kycLevel > 0) {
|
||||
_setVelocityLimits(
|
||||
account,
|
||||
kycLevelDailyLimits[kycRecord.kycLevel],
|
||||
kycLevelMonthlyLimits[kycRecord.kycLevel]
|
||||
);
|
||||
} else {
|
||||
_setVelocityLimits(account, defaultDailyLimit, defaultMonthlyLimit);
|
||||
}
|
||||
}
|
||||
|
||||
uint256 today = block.timestamp / 1 days;
|
||||
uint256 thisMonth = block.timestamp / 30 days;
|
||||
|
||||
// Reset daily counter if new day
|
||||
if (today > limits.lastDayReset) {
|
||||
limits.dailySpent = 0;
|
||||
limits.lastDayReset = today;
|
||||
}
|
||||
|
||||
// Reset monthly counter if new month
|
||||
if (thisMonth > limits.lastMonthReset) {
|
||||
limits.monthlySpent = 0;
|
||||
limits.lastMonthReset = thisMonth;
|
||||
}
|
||||
|
||||
// Update spending
|
||||
limits.dailySpent += amount;
|
||||
limits.monthlySpent += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if transaction should trigger AML reporting
|
||||
*/
|
||||
function checkReportingThresholds(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) external view override returns (bool shouldReport, uint8 reportType) {
|
||||
// CTR - Currency Transaction Report (large transactions)
|
||||
if (amount >= ctrThreshold) {
|
||||
return (true, 1);
|
||||
}
|
||||
|
||||
// SAR - Suspicious Activity Report (pattern analysis would go here)
|
||||
if (amount >= sarThreshold) {
|
||||
// In practice, this would include more sophisticated pattern analysis
|
||||
// For now, just check if it's a large transaction to/from new addresses
|
||||
KYCRecord memory fromRecord = kycRecords[from];
|
||||
KYCRecord memory toRecord = kycRecords[to];
|
||||
|
||||
if (fromRecord.whitelistTime == 0 || toRecord.whitelistTime == 0) {
|
||||
return (true, 2);
|
||||
}
|
||||
|
||||
// Check if accounts were recently whitelisted (< 7 days)
|
||||
if (block.timestamp - fromRecord.whitelistTime < 7 days ||
|
||||
block.timestamp - toRecord.whitelistTime < 7 days) {
|
||||
return (true, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get KYC record for address
|
||||
*/
|
||||
function getKYCRecord(address account) external view override returns (KYCRecord memory) {
|
||||
return kycRecords[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get velocity limits for address
|
||||
*/
|
||||
function getVelocityLimits(address account) external view override returns (VelocityLimits memory) {
|
||||
return velocityLimits[account];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update reporting thresholds
|
||||
*/
|
||||
function updateReportingThresholds(
|
||||
uint256 newCtrThreshold,
|
||||
uint256 newSarThreshold
|
||||
) external onlyRole(AML_MANAGER_ROLE) {
|
||||
require(newCtrThreshold > 0, "CTR threshold must be positive");
|
||||
require(newSarThreshold > 0, "SAR threshold must be positive");
|
||||
|
||||
ctrThreshold = newCtrThreshold;
|
||||
sarThreshold = newSarThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update KYC level limits
|
||||
*/
|
||||
function updateKYCLevelLimits(
|
||||
uint256 kycLevel,
|
||||
uint256 dailyLimit,
|
||||
uint256 monthlyLimit
|
||||
) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(kycLevel >= 1 && kycLevel <= 3, "Invalid KYC level");
|
||||
require(dailyLimit > 0 && monthlyLimit > 0, "Limits must be positive");
|
||||
require(monthlyLimit >= dailyLimit, "Monthly must be >= daily");
|
||||
|
||||
kycLevelDailyLimits[kycLevel] = dailyLimit;
|
||||
kycLevelMonthlyLimits[kycLevel] = monthlyLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause compliance operations
|
||||
*/
|
||||
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause compliance operations
|
||||
*/
|
||||
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
_unpause();
|
||||
}
|
||||
}
|
||||
259
contracts/core/MeridianToken.sol
Normal file
259
contracts/core/MeridianToken.sol
Normal file
@@ -0,0 +1,259 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "../interfaces/IReserveAggregator.sol";
|
||||
import "../interfaces/ICompliance.sol";
|
||||
|
||||
/**
|
||||
* @title MeridianToken
|
||||
* @dev Core token contract implementing the Meridian Protocol Standard
|
||||
* @notice Regulated entities can issue tokens backed by reserves with built-in compliance
|
||||
*/
|
||||
contract MeridianToken is ERC20, ERC20Permit, AccessControl, Pausable, ReentrancyGuard {
|
||||
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
||||
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
|
||||
bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");
|
||||
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
|
||||
|
||||
// Reserve and compliance contracts
|
||||
IReserveAggregator public reserveAggregator;
|
||||
ICompliance public compliance;
|
||||
|
||||
// Token metadata
|
||||
string public currency; // e.g., "GBP", "USD", "EUR"
|
||||
string public issuerName; // e.g., "Meridian LLC", "HSBC"
|
||||
uint256 public minimumCollateralRatio; // e.g., 102% = 10200 (basis points * 100)
|
||||
|
||||
// Minting controls
|
||||
uint256 public dailyMintLimit;
|
||||
uint256 public dailyBurnLimit;
|
||||
mapping(uint256 => uint256) public dailyMinted; // day => amount minted
|
||||
mapping(uint256 => uint256) public dailyBurned; // day => amount burned
|
||||
|
||||
// Events
|
||||
event Minted(address indexed to, uint256 amount, bytes32 attestationHash);
|
||||
event Burned(address indexed from, uint256 amount, bytes32 attestationHash);
|
||||
event ReserveAggregatorUpdated(address indexed newAggregator);
|
||||
event ComplianceUpdated(address indexed newCompliance);
|
||||
event DailyLimitsUpdated(uint256 newMintLimit, uint256 newBurnLimit);
|
||||
event MinimumCollateralRatioUpdated(uint256 newRatio);
|
||||
|
||||
/**
|
||||
* @dev Constructor sets up the token with initial parameters
|
||||
* @param name Token name (e.g., "Meridian GBP")
|
||||
* @param symbol Token symbol (e.g., "MGBP")
|
||||
* @param _currency Currency denomination (e.g., "GBP")
|
||||
* @param _issuerName Name of issuing entity
|
||||
* @param _reserveAggregator Address of reserve aggregator contract
|
||||
* @param _compliance Address of compliance contract
|
||||
* @param _admin Address to receive admin role
|
||||
*/
|
||||
constructor(
|
||||
string memory name,
|
||||
string memory symbol,
|
||||
string memory _currency,
|
||||
string memory _issuerName,
|
||||
address _reserveAggregator,
|
||||
address _compliance,
|
||||
address _admin
|
||||
) ERC20(name, symbol) ERC20Permit(name) {
|
||||
require(_reserveAggregator != address(0), "Invalid reserve aggregator");
|
||||
require(_compliance != address(0), "Invalid compliance contract");
|
||||
require(_admin != address(0), "Invalid admin address");
|
||||
|
||||
currency = _currency;
|
||||
issuerName = _issuerName;
|
||||
reserveAggregator = IReserveAggregator(_reserveAggregator);
|
||||
compliance = ICompliance(_compliance);
|
||||
minimumCollateralRatio = 10200; // 102% default
|
||||
|
||||
dailyMintLimit = 10000000 * 10**decimals(); // 10M default
|
||||
dailyBurnLimit = 10000000 * 10**decimals(); // 10M default
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
|
||||
_grantRole(MINTER_ROLE, _admin);
|
||||
_grantRole(BURNER_ROLE, _admin);
|
||||
_grantRole(COMPLIANCE_ROLE, _admin);
|
||||
_grantRole(PAUSER_ROLE, _admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Mint tokens with reserve attestation and compliance checks
|
||||
* @param to Address to mint tokens to
|
||||
* @param amount Amount to mint
|
||||
* @param attestationHash Hash of reserve attestation document
|
||||
*/
|
||||
function mint(
|
||||
address to,
|
||||
uint256 amount,
|
||||
bytes32 attestationHash
|
||||
) external onlyRole(MINTER_ROLE) nonReentrant whenNotPaused {
|
||||
require(to != address(0), "Cannot mint to zero address");
|
||||
require(amount > 0, "Amount must be positive");
|
||||
|
||||
// Check daily mint limit
|
||||
uint256 today = block.timestamp / 86400; // days since epoch
|
||||
require(dailyMinted[today] + amount <= dailyMintLimit, "Daily mint limit exceeded");
|
||||
|
||||
// Check compliance
|
||||
require(compliance.isWhitelisted(to), "Recipient not whitelisted");
|
||||
require(!compliance.isSanctioned(to), "Recipient is sanctioned");
|
||||
|
||||
// Check reserve collateralization after mint
|
||||
uint256 newTotalSupply = totalSupply() + amount;
|
||||
(uint256 reserveValue, bool isValid) = reserveAggregator.getTotalReserveValue();
|
||||
require(isValid, "Reserve data is stale");
|
||||
|
||||
uint256 requiredReserves = (newTotalSupply * minimumCollateralRatio) / 10000;
|
||||
require(reserveValue >= requiredReserves, "Insufficient reserves for mint");
|
||||
|
||||
// Update daily tracking
|
||||
dailyMinted[today] += amount;
|
||||
|
||||
// Mint tokens
|
||||
_mint(to, amount);
|
||||
|
||||
emit Minted(to, amount, attestationHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Burn tokens with attestation
|
||||
* @param from Address to burn tokens from
|
||||
* @param amount Amount to burn
|
||||
* @param attestationHash Hash of burn attestation document
|
||||
*/
|
||||
function burn(
|
||||
address from,
|
||||
uint256 amount,
|
||||
bytes32 attestationHash
|
||||
) external onlyRole(BURNER_ROLE) nonReentrant whenNotPaused {
|
||||
require(from != address(0), "Cannot burn from zero address");
|
||||
require(amount > 0, "Amount must be positive");
|
||||
require(balanceOf(from) >= amount, "Insufficient balance");
|
||||
|
||||
// Check daily burn limit
|
||||
uint256 today = block.timestamp / 86400;
|
||||
require(dailyBurned[today] + amount <= dailyBurnLimit, "Daily burn limit exceeded");
|
||||
|
||||
// Update daily tracking
|
||||
dailyBurned[today] += amount;
|
||||
|
||||
// Burn tokens
|
||||
_burn(from, amount);
|
||||
|
||||
emit Burned(from, amount, attestationHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Override transfer to include compliance checks
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) internal virtual override {
|
||||
super._beforeTokenTransfer(from, to, amount);
|
||||
|
||||
// Skip compliance for minting/burning (handled in mint/burn functions)
|
||||
if (from == address(0) || to == address(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check compliance for regular transfers
|
||||
require(!paused(), "Token transfers paused");
|
||||
require(compliance.isWhitelisted(to), "Recipient not whitelisted");
|
||||
require(!compliance.isSanctioned(from), "Sender is sanctioned");
|
||||
require(!compliance.isSanctioned(to), "Recipient is sanctioned");
|
||||
|
||||
// Check velocity limits
|
||||
require(compliance.checkVelocityLimits(from, amount), "Velocity limit exceeded");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get current collateralization ratio
|
||||
* @return ratio Current ratio in basis points * 100 (e.g., 10200 = 102%)
|
||||
* @return isValid Whether the reserve data is current and valid
|
||||
*/
|
||||
function getCurrentCollateralizationRatio() external view returns (uint256 ratio, bool isValid) {
|
||||
(uint256 reserveValue, bool _isValid) = reserveAggregator.getTotalReserveValue();
|
||||
if (!_isValid || totalSupply() == 0) {
|
||||
return (0, false);
|
||||
}
|
||||
ratio = (reserveValue * 10000) / totalSupply();
|
||||
return (ratio, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update reserve aggregator contract
|
||||
*/
|
||||
function updateReserveAggregator(address newAggregator) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(newAggregator != address(0), "Invalid aggregator address");
|
||||
reserveAggregator = IReserveAggregator(newAggregator);
|
||||
emit ReserveAggregatorUpdated(newAggregator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update compliance contract
|
||||
*/
|
||||
function updateCompliance(address newCompliance) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(newCompliance != address(0), "Invalid compliance address");
|
||||
compliance = ICompliance(newCompliance);
|
||||
emit ComplianceUpdated(newCompliance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update daily mint/burn limits
|
||||
*/
|
||||
function updateDailyLimits(
|
||||
uint256 newMintLimit,
|
||||
uint256 newBurnLimit
|
||||
) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
dailyMintLimit = newMintLimit;
|
||||
dailyBurnLimit = newBurnLimit;
|
||||
emit DailyLimitsUpdated(newMintLimit, newBurnLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update minimum collateral ratio
|
||||
*/
|
||||
function updateMinimumCollateralRatio(uint256 newRatio) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
||||
require(newRatio >= 10000, "Ratio must be at least 100%");
|
||||
minimumCollateralRatio = newRatio;
|
||||
emit MinimumCollateralRatioUpdated(newRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Pause token operations
|
||||
*/
|
||||
function pause() external onlyRole(PAUSER_ROLE) {
|
||||
_pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Unpause token operations
|
||||
*/
|
||||
function unpause() external onlyRole(PAUSER_ROLE) {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get remaining mint capacity for today
|
||||
*/
|
||||
function getRemainingMintCapacity() external view returns (uint256) {
|
||||
uint256 today = block.timestamp / 86400;
|
||||
return dailyMintLimit - dailyMinted[today];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get remaining burn capacity for today
|
||||
*/
|
||||
function getRemainingBurnCapacity() external view returns (uint256) {
|
||||
uint256 today = block.timestamp / 86400;
|
||||
return dailyBurnLimit - dailyBurned[today];
|
||||
}
|
||||
}
|
||||
88
contracts/interfaces/ICompliance.sol
Normal file
88
contracts/interfaces/ICompliance.sol
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title ICompliance
|
||||
* @dev Interface for KYC, sanctions screening, and AML compliance
|
||||
*/
|
||||
interface ICompliance {
|
||||
struct KYCRecord {
|
||||
bool isWhitelisted;
|
||||
uint256 kycLevel; // 1=basic, 2=enhanced, 3=institutional
|
||||
uint256 whitelistTime;
|
||||
uint256 expiryTime;
|
||||
string jurisdiction;
|
||||
}
|
||||
|
||||
struct VelocityLimits {
|
||||
uint256 dailyLimit;
|
||||
uint256 monthlyLimit;
|
||||
uint256 dailySpent;
|
||||
uint256 monthlySpent;
|
||||
uint256 lastDayReset;
|
||||
uint256 lastMonthReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if address is whitelisted for KYC
|
||||
* @param account Address to check
|
||||
* @return isWhitelisted True if account passed KYC
|
||||
*/
|
||||
function isWhitelisted(address account) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Check if address is on sanctions list
|
||||
* @param account Address to check
|
||||
* @return isSanctioned True if account is sanctioned
|
||||
*/
|
||||
function isSanctioned(address account) external view returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Check velocity limits for transaction
|
||||
* @param account Address making transaction
|
||||
* @param amount Transaction amount
|
||||
* @return withinLimits True if transaction is within velocity limits
|
||||
*/
|
||||
function checkVelocityLimits(address account, uint256 amount) external view returns (bool withinLimits);
|
||||
|
||||
/**
|
||||
* @dev Get KYC record for address
|
||||
* @param account Address to query
|
||||
*/
|
||||
function getKYCRecord(address account) external view returns (KYCRecord memory);
|
||||
|
||||
/**
|
||||
* @dev Get velocity limits for address
|
||||
* @param account Address to query
|
||||
*/
|
||||
function getVelocityLimits(address account) external view returns (VelocityLimits memory);
|
||||
|
||||
/**
|
||||
* @dev Update velocity tracking after transaction
|
||||
* @param account Address that made transaction
|
||||
* @param amount Transaction amount
|
||||
*/
|
||||
function updateVelocityTracking(address account, uint256 amount) external;
|
||||
|
||||
/**
|
||||
* @dev Check if transaction should trigger AML reporting
|
||||
* @param from Sender address
|
||||
* @param to Recipient address
|
||||
* @param amount Transaction amount
|
||||
* @return shouldReport True if transaction exceeds reporting thresholds
|
||||
* @return reportType Type of report needed (1=CTR, 2=SAR, etc.)
|
||||
*/
|
||||
function checkReportingThresholds(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) external view returns (bool shouldReport, uint8 reportType);
|
||||
|
||||
// Events
|
||||
event AccountWhitelisted(address indexed account, uint256 kycLevel, string jurisdiction);
|
||||
event AccountBlacklisted(address indexed account, string reason);
|
||||
event SanctionsListUpdated(uint256 newListHash);
|
||||
event VelocityLimitsUpdated(address indexed account, uint256 dailyLimit, uint256 monthlyLimit);
|
||||
event ReportingThresholdTriggered(address indexed from, address indexed to, uint256 amount, uint8 reportType);
|
||||
event VelocityLimitExceeded(address indexed account, uint256 attempted, uint256 remaining);
|
||||
}
|
||||
66
contracts/interfaces/IReserveAggregator.sol
Normal file
66
contracts/interfaces/IReserveAggregator.sol
Normal file
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title IReserveAggregator
|
||||
* @dev Interface for reserve aggregation from multiple custodians
|
||||
*/
|
||||
interface IReserveAggregator {
|
||||
struct CustodianInfo {
|
||||
string name;
|
||||
address oracle;
|
||||
uint256 lastUpdateTime;
|
||||
uint256 heartbeat; // maximum staleness in seconds
|
||||
bool isActive;
|
||||
uint8 custodianType; // 0=bank, 1=crypto, 2=fund_admin
|
||||
}
|
||||
|
||||
struct ReserveAttestation {
|
||||
uint256 balance;
|
||||
uint256 timestamp;
|
||||
bytes32 documentHash;
|
||||
address attestor;
|
||||
bool isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Get total reserve value across all active custodians
|
||||
* @return totalValue Total reserve value in token denomination
|
||||
* @return isValid Whether all required custodian data is current
|
||||
*/
|
||||
function getTotalReserveValue() external view returns (uint256 totalValue, bool isValid);
|
||||
|
||||
/**
|
||||
* @dev Get reserve value from specific custodian
|
||||
* @param custodianId Unique identifier for custodian
|
||||
* @return value Reserve value from this custodian
|
||||
* @return isValid Whether this custodian's data is current
|
||||
*/
|
||||
function getCustodianReserveValue(bytes32 custodianId) external view returns (uint256 value, bool isValid);
|
||||
|
||||
/**
|
||||
* @dev Get custodian information
|
||||
* @param custodianId Unique identifier for custodian
|
||||
*/
|
||||
function getCustodianInfo(bytes32 custodianId) external view returns (CustodianInfo memory);
|
||||
|
||||
/**
|
||||
* @dev Get latest attestation for a custodian
|
||||
* @param custodianId Unique identifier for custodian
|
||||
*/
|
||||
function getLatestAttestation(bytes32 custodianId) external view returns (ReserveAttestation memory);
|
||||
|
||||
/**
|
||||
* @dev Check if reserve data is stale for any custodian
|
||||
* @return isStale True if any custodian data exceeds heartbeat
|
||||
* @return staleCustodians Array of custodian IDs with stale data
|
||||
*/
|
||||
function checkDataStaleness() external view returns (bool isStale, bytes32[] memory staleCustodians);
|
||||
|
||||
// Events
|
||||
event CustodianAdded(bytes32 indexed custodianId, string name, address oracle);
|
||||
event CustodianUpdated(bytes32 indexed custodianId, address newOracle, uint256 newHeartbeat);
|
||||
event CustodianDeactivated(bytes32 indexed custodianId);
|
||||
event ReserveAttested(bytes32 indexed custodianId, uint256 balance, bytes32 documentHash);
|
||||
event StaleDataDetected(bytes32 indexed custodianId, uint256 lastUpdate, uint256 heartbeat);
|
||||
}
|
||||
47
hardhat.config.js
Normal file
47
hardhat.config.js
Normal file
@@ -0,0 +1,47 @@
|
||||
require("@nomicfoundation/hardhat-toolbox");
|
||||
require("dotenv").config();
|
||||
|
||||
/** @type import('hardhat/config').HardhatUserConfig */
|
||||
module.exports = {
|
||||
solidity: {
|
||||
version: "0.8.20",
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1337
|
||||
},
|
||||
amitis: {
|
||||
url: "https://rpc.amitis.network",
|
||||
chainId: 2049, // Amitis mainnet chain ID
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
||||
},
|
||||
sepolia: {
|
||||
url: `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
chainId: 11155111,
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
||||
},
|
||||
mainnet: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
chainId: 1,
|
||||
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
|
||||
}
|
||||
},
|
||||
etherscan: {
|
||||
apiKey: {
|
||||
sepolia: process.env.ETHERSCAN_API_KEY,
|
||||
mainnet: process.env.ETHERSCAN_API_KEY
|
||||
}
|
||||
},
|
||||
paths: {
|
||||
sources: "./contracts",
|
||||
tests: "./test",
|
||||
cache: "./cache",
|
||||
artifacts: "./artifacts"
|
||||
}
|
||||
};
|
||||
7974
package-lock.json
generated
Normal file
7974
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "meridian-protocol",
|
||||
"version": "1.0.0",
|
||||
"description": "Meridian Protocol - Monetary Operating System for Programmable Money",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"compile": "hardhat compile",
|
||||
"test": "hardhat test",
|
||||
"deploy": "hardhat run scripts/deploy.js",
|
||||
"deploy:sepolia": "hardhat run scripts/deploy.js --network sepolia",
|
||||
"deploy:amitis": "hardhat run scripts/deploy.js --network amitis",
|
||||
"deploy:mainnet": "hardhat run scripts/deploy.js --network mainnet"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.9.0",
|
||||
"@openzeppelin/contracts-upgradeable": "^4.9.0",
|
||||
"@chainlink/contracts": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
|
||||
"hardhat": "^2.19.0",
|
||||
"dotenv": "^16.3.0"
|
||||
},
|
||||
"keywords": [
|
||||
"meridian",
|
||||
"stablecoin",
|
||||
"programmable-money",
|
||||
"defi",
|
||||
"compliance",
|
||||
"reserves"
|
||||
],
|
||||
"author": "Meridian Protocol",
|
||||
"license": "MIT"
|
||||
}
|
||||
113
scripts/deploy.js
Normal file
113
scripts/deploy.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const { ethers } = require("hardhat");
|
||||
|
||||
async function main() {
|
||||
console.log("Deploying Meridian Protocol contracts...");
|
||||
|
||||
const [deployer] = await ethers.getSigners();
|
||||
console.log("Deploying with account:", deployer.address);
|
||||
console.log("Account balance:", (await deployer.getBalance()).toString());
|
||||
|
||||
// 1. Deploy Compliance contract
|
||||
console.log("\n1. Deploying Compliance contract...");
|
||||
const Compliance = await ethers.getContractFactory("Compliance");
|
||||
const compliance = await Compliance.deploy(deployer.address);
|
||||
await compliance.deployed();
|
||||
console.log("Compliance deployed to:", compliance.address);
|
||||
|
||||
// 2. Deploy ReserveAggregator contract
|
||||
console.log("\n2. Deploying ReserveAggregator contract...");
|
||||
const ReserveAggregator = await ethers.getContractFactory("ReserveAggregator");
|
||||
const reserveAggregator = await ReserveAggregator.deploy(deployer.address);
|
||||
await reserveAggregator.deployed();
|
||||
console.log("ReserveAggregator deployed to:", reserveAggregator.address);
|
||||
|
||||
// 3. Deploy MeridianToken for GBP
|
||||
console.log("\n3. Deploying MeridianToken (MGBP)...");
|
||||
const MeridianToken = await ethers.getContractFactory("MeridianToken");
|
||||
const mgbp = await MeridianToken.deploy(
|
||||
"Meridian GBP", // name
|
||||
"MGBP", // symbol
|
||||
"GBP", // currency
|
||||
"Meridian LLC", // issuer name
|
||||
reserveAggregator.address,
|
||||
compliance.address,
|
||||
deployer.address // admin
|
||||
);
|
||||
await mgbp.deployed();
|
||||
console.log("MGBP Token deployed to:", mgbp.address);
|
||||
|
||||
// 4. Configure initial settings
|
||||
console.log("\n4. Configuring initial settings...");
|
||||
|
||||
// Set up KYC levels in compliance contract
|
||||
await compliance.updateKYCLevelLimits(1, ethers.utils.parseEther("1000"), ethers.utils.parseEther("10000"));
|
||||
await compliance.updateKYCLevelLimits(2, ethers.utils.parseEther("10000"), ethers.utils.parseEther("100000"));
|
||||
await compliance.updateKYCLevelLimits(3, ethers.utils.parseEther("100000"), ethers.utils.parseEther("1000000"));
|
||||
|
||||
// Add deployer to KYC whitelist for testing
|
||||
await compliance.whitelistAddress(deployer.address, 3, "UK", 0);
|
||||
console.log("Deployer whitelisted for testing");
|
||||
|
||||
// 5. Add initial custodian (placeholder for Anchorage)
|
||||
console.log("\n5. Adding initial custodian...");
|
||||
const custodianId = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("anchorage-gbp"));
|
||||
await reserveAggregator.addCustodian(
|
||||
custodianId,
|
||||
"Anchorage Digital GBP",
|
||||
deployer.address, // placeholder oracle address
|
||||
3600, // 1 hour heartbeat
|
||||
2 // fund admin type for manual attestation
|
||||
);
|
||||
console.log("Initial custodian added");
|
||||
|
||||
// 6. Display deployment summary
|
||||
console.log("\n=== DEPLOYMENT SUMMARY ===");
|
||||
console.log("Network:", network.name);
|
||||
console.log("Deployer:", deployer.address);
|
||||
console.log("");
|
||||
console.log("Contract Addresses:");
|
||||
console.log("- Compliance:", compliance.address);
|
||||
console.log("- ReserveAggregator:", reserveAggregator.address);
|
||||
console.log("- MGBP Token:", mgbp.address);
|
||||
console.log("");
|
||||
console.log("Token Details:");
|
||||
console.log("- Name:", await mgbp.name());
|
||||
console.log("- Symbol:", await mgbp.symbol());
|
||||
console.log("- Currency:", await mgbp.currency());
|
||||
console.log("- Issuer:", await mgbp.issuerName());
|
||||
console.log("- Min Collateral Ratio:", (await mgbp.minimumCollateralRatio()).toString() + " (basis points * 100)");
|
||||
console.log("");
|
||||
console.log("Next Steps:");
|
||||
console.log("1. Add real custodian oracles to ReserveAggregator");
|
||||
console.log("2. Attest initial reserves");
|
||||
console.log("3. Set up Chainlink PoR feeds");
|
||||
console.log("4. Configure production KYC whitelist");
|
||||
console.log("5. Verify contracts on Etherscan");
|
||||
|
||||
// Save deployment addresses to file
|
||||
const fs = require('fs');
|
||||
const deploymentInfo = {
|
||||
network: network.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
deployer: deployer.address,
|
||||
contracts: {
|
||||
Compliance: compliance.address,
|
||||
ReserveAggregator: reserveAggregator.address,
|
||||
MGBP: mgbp.address
|
||||
},
|
||||
custodianId: custodianId
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
`deployment-${network.name}.json`,
|
||||
JSON.stringify(deploymentInfo, null, 2)
|
||||
);
|
||||
console.log(`\nDeployment info saved to deployment-${network.name}.json`);
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
182
test/MeridianToken.test.js
Normal file
182
test/MeridianToken.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const { expect } = require("chai");
|
||||
const { ethers } = require("hardhat");
|
||||
|
||||
describe("Meridian Protocol", function () {
|
||||
let meridianToken, compliance, reserveAggregator;
|
||||
let owner, addr1, addr2;
|
||||
let custodianId;
|
||||
|
||||
beforeEach(async function () {
|
||||
[owner, addr1, addr2] = await ethers.getSigners();
|
||||
|
||||
// Deploy Compliance
|
||||
const Compliance = await ethers.getContractFactory("Compliance");
|
||||
compliance = await Compliance.deploy(owner.address);
|
||||
await compliance.deployed();
|
||||
|
||||
// Deploy ReserveAggregator
|
||||
const ReserveAggregator = await ethers.getContractFactory("ReserveAggregator");
|
||||
reserveAggregator = await ReserveAggregator.deploy(owner.address);
|
||||
await reserveAggregator.deployed();
|
||||
|
||||
// Deploy MeridianToken
|
||||
const MeridianToken = await ethers.getContractFactory("MeridianToken");
|
||||
meridianToken = await MeridianToken.deploy(
|
||||
"Meridian GBP",
|
||||
"MGBP",
|
||||
"GBP",
|
||||
"Meridian LLC",
|
||||
reserveAggregator.address,
|
||||
compliance.address,
|
||||
owner.address
|
||||
);
|
||||
await meridianToken.deployed();
|
||||
|
||||
// Add test custodian
|
||||
custodianId = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("test-custodian"));
|
||||
await reserveAggregator.addCustodian(
|
||||
custodianId,
|
||||
"Test Custodian",
|
||||
owner.address, // Use owner as oracle for testing
|
||||
3600, // 1 hour heartbeat
|
||||
2 // fund admin type
|
||||
);
|
||||
});
|
||||
|
||||
describe("Deployment", function () {
|
||||
it("Should deploy with correct parameters", async function () {
|
||||
expect(await meridianToken.name()).to.equal("Meridian GBP");
|
||||
expect(await meridianToken.symbol()).to.equal("MGBP");
|
||||
expect(await meridianToken.currency()).to.equal("GBP");
|
||||
expect(await meridianToken.issuerName()).to.equal("Meridian LLC");
|
||||
expect(await meridianToken.minimumCollateralRatio()).to.equal(10200);
|
||||
});
|
||||
|
||||
it("Should have correct access roles", async function () {
|
||||
const MINTER_ROLE = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("MINTER_ROLE"));
|
||||
expect(await meridianToken.hasRole(MINTER_ROLE, owner.address)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Compliance", function () {
|
||||
it("Should whitelist addresses correctly", async function () {
|
||||
await compliance.whitelistAddress(addr1.address, 2, "UK", 0);
|
||||
expect(await compliance.isWhitelisted(addr1.address)).to.be.true;
|
||||
expect(await compliance.isSanctioned(addr1.address)).to.be.false;
|
||||
});
|
||||
|
||||
it("Should set velocity limits based on KYC level", async function () {
|
||||
await compliance.whitelistAddress(addr1.address, 1, "UK", 0);
|
||||
const limits = await compliance.getVelocityLimits(addr1.address);
|
||||
expect(limits.dailyLimit).to.equal(ethers.utils.parseEther("1000"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reserve Aggregator", function () {
|
||||
it("Should add custodians correctly", async function () {
|
||||
const custodianInfo = await reserveAggregator.getCustodianInfo(custodianId);
|
||||
expect(custodianInfo.name).to.equal("Test Custodian");
|
||||
expect(custodianInfo.isActive).to.be.true;
|
||||
expect(custodianInfo.custodianType).to.equal(2);
|
||||
});
|
||||
|
||||
it("Should attest reserves", async function () {
|
||||
const reserveAmount = ethers.utils.parseEther("1020000");
|
||||
const documentHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("reserve-doc"));
|
||||
|
||||
await reserveAggregator.attestReserves(
|
||||
custodianId,
|
||||
reserveAmount,
|
||||
documentHash
|
||||
);
|
||||
|
||||
const attestation = await reserveAggregator.getLatestAttestation(custodianId);
|
||||
expect(attestation.balance).to.equal(reserveAmount);
|
||||
expect(attestation.isValid).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Token Operations", function () {
|
||||
beforeEach(async function () {
|
||||
// Whitelist recipient
|
||||
await compliance.whitelistAddress(addr1.address, 2, "UK", 0);
|
||||
|
||||
// Attest reserves
|
||||
const reserveAmount = ethers.utils.parseEther("1020000"); // £1.02M
|
||||
const documentHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("reserve-doc"));
|
||||
await reserveAggregator.attestReserves(custodianId, reserveAmount, documentHash);
|
||||
});
|
||||
|
||||
it("Should mint tokens with sufficient reserves", async function () {
|
||||
const mintAmount = ethers.utils.parseEther("1000000"); // £1M
|
||||
const attestationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mint-attestation"));
|
||||
|
||||
await meridianToken.mint(addr1.address, mintAmount, attestationHash);
|
||||
|
||||
expect(await meridianToken.balanceOf(addr1.address)).to.equal(mintAmount);
|
||||
expect(await meridianToken.totalSupply()).to.equal(mintAmount);
|
||||
});
|
||||
|
||||
it("Should reject mint with insufficient reserves", async function () {
|
||||
const mintAmount = ethers.utils.parseEther("1100000"); // £1.1M (exceeds 102% ratio)
|
||||
const attestationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mint-attestation"));
|
||||
|
||||
await expect(
|
||||
meridianToken.mint(addr1.address, mintAmount, attestationHash)
|
||||
).to.be.revertedWith("Insufficient reserves for mint");
|
||||
});
|
||||
|
||||
it("Should reject mint to non-whitelisted address", async function () {
|
||||
const mintAmount = ethers.utils.parseEther("1000");
|
||||
const attestationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mint-attestation"));
|
||||
|
||||
await expect(
|
||||
meridianToken.mint(addr2.address, mintAmount, attestationHash)
|
||||
).to.be.revertedWith("Recipient not whitelisted");
|
||||
});
|
||||
|
||||
it("Should calculate collateralization ratio correctly", async function () {
|
||||
const mintAmount = ethers.utils.parseEther("1000000");
|
||||
const attestationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mint-attestation"));
|
||||
|
||||
await meridianToken.mint(addr1.address, mintAmount, attestationHash);
|
||||
|
||||
const [ratio, isValid] = await meridianToken.getCurrentCollateralizationRatio();
|
||||
expect(isValid).to.be.true;
|
||||
expect(ratio).to.equal(10200); // 102%
|
||||
});
|
||||
});
|
||||
|
||||
describe("Transfer Restrictions", function () {
|
||||
beforeEach(async function () {
|
||||
// Setup: mint tokens to addr1
|
||||
await compliance.whitelistAddress(addr1.address, 2, "UK", 0);
|
||||
await compliance.whitelistAddress(addr2.address, 2, "US", 0);
|
||||
|
||||
const reserveAmount = ethers.utils.parseEther("1020000");
|
||||
const documentHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("reserve-doc"));
|
||||
await reserveAggregator.attestReserves(custodianId, reserveAmount, documentHash);
|
||||
|
||||
const mintAmount = ethers.utils.parseEther("1000");
|
||||
const attestationHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mint-attestation"));
|
||||
await meridianToken.mint(addr1.address, mintAmount, attestationHash);
|
||||
});
|
||||
|
||||
it("Should allow transfer between whitelisted addresses", async function () {
|
||||
const transferAmount = ethers.utils.parseEther("100");
|
||||
|
||||
await meridianToken.connect(addr1).transfer(addr2.address, transferAmount);
|
||||
|
||||
expect(await meridianToken.balanceOf(addr2.address)).to.equal(transferAmount);
|
||||
});
|
||||
|
||||
it("Should reject transfer to non-whitelisted address", async function () {
|
||||
const transferAmount = ethers.utils.parseEther("100");
|
||||
const addr3 = ethers.Wallet.createRandom();
|
||||
|
||||
await expect(
|
||||
meridianToken.connect(addr1).transfer(addr3.address, transferAmount)
|
||||
).to.be.revertedWith("Recipient not whitelisted");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user