diff --git a/README.md b/README.md index e0d82ef..2f15609 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,263 @@ # OpenGradient Examples and Templates - -Welcome to the Examples and Templates repository! πŸš€ This repo is designed to help developers quickly get started with our products by providing examples and templates. -## What You Will Find Here +Welcome to the Examples and Templates repository! This repo provides production-ready examples demonstrating how to integrate OpenGradient's decentralized AI infrastructure into smart contracts. - - Code Examples: Real-world use cases demonstrating how to use OpenGradient products, from basic setups to advanced integrations. +## Overview -## Coming soon - - Templates: Ready-to-use project templates that can serve as a starting point for building your own applications, including boilerplate code and pre-configured settings. - - Step-by-Step Guides: Detailed instructions to help you understand how each example works and how to adapt the templates for your specific needs. - - Best Practices: Tips and recommendations on optimizing your code, following security guidelines, and making the most of our tools. +OpenGradient enables **verifiable AI inference on-chain** through: +- **TEE-secured ML models**: Run machine learning models with hardware-level security guarantees +- **LLM chat inference**: Use language models for natural language processing tasks +- **ZK-verified computations**: Cryptographic proofs for AI model outputs +- **Historical data access**: Query on-chain price feeds and market data -## How to Use This Repository - - Clone or download the examples and templates that match your project requirements. - - Follow OpenGradient [documentation](https://docs.opengradient.ai/) to set up and customize the code for your own use case. +## Examples + +### DeFi Examples + +#### 1. Dynamic Fee AMM (`contracts/defi/DynamicFeeAMM.sol`) + +An Automated Market Maker that uses ML-predicted volatility to adjust swap fees dynamically. + +**Features:** +- Real-time volatility prediction using OGHistorical price data +- Dynamic fee calculation (1-100 bps based on volatility) +- ZK-verified ML model inference +- Statistical preprocessing with OGPreprocessing + +**Architecture:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Swap │────▢│ OGHistorical │────▢│ Price History β”‚ +β”‚ Request β”‚ β”‚ (Last 24 Prices)β”‚ β”‚ (24h Window) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Execute Swap │◀────│ Calculate Fee │◀────│ ML Volatility β”‚ +β”‚ with Fee β”‚ β”‚ (1-100 bps) β”‚ β”‚ Prediction β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Use Cases:** +- DEX protocols seeking fair, market-responsive fees +- Liquidity providers wanting volatility-adjusted returns +- DeFi protocols needing on-chain volatility oracles + +--- + +#### 2. Lending Risk Assessor (`contracts/defi/LendingRiskAssessor.sol`) + +An ML-powered lending protocol that assesses borrower risk and sets personalized rates. + +**Features:** +- User profiling based on collateral, debt, and transaction history +- TEE-verified ML risk scoring (0-100 scale) +- Dynamic interest rates (5-30% based on risk) +- Dynamic collateral ratios (150-300% based on risk) +- Real-time position health monitoring + +**Architecture:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Borrower │────▢│ Build Profile │────▢│ ML Risk Model β”‚ +β”‚ Request β”‚ β”‚ (History/Stats) β”‚ β”‚ (TEE-Secured) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Loan Issued │◀────│ Risk-Adjusted │◀────│ Risk Score β”‚ +β”‚ w/ Terms β”‚ β”‚ Rate & Ratio β”‚ β”‚ (0-100) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Use Cases:** +- DeFi lending protocols (Aave, Compound style) +- Under-collateralized lending with ML credit scoring +- Risk-based insurance protocols + +--- + +### NFT Examples + +#### 3. Sentiment-Gated NFT Mint (`contracts/nft/SentimentGatedMint.sol`) + +An NFT collection where minting eligibility is determined by LLM sentiment analysis. + +**Features:** +- LLM-powered sentiment analysis of mint applications +- TEE-secured inference for tamper-proof decisions +- Configurable sentiment threshold +- Appeal mechanism for rejected applications +- Full audit trail of AI decisions + +**Architecture:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Submits │────▢│ LLM Sentiment │────▢│ Parse Response β”‚ +β”‚ Statement β”‚ β”‚ Analysis (TEE) β”‚ β”‚ APPROVED/ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ REJECTED β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ NFT Minted │◀────│ Threshold Check │◀────│ Application β”‚ +β”‚ (if approved) β”‚ β”‚ (configurable) β”‚ β”‚ Stored β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Use Cases:** +- Curated NFT collections with quality control +- Community membership NFTs with values alignment +- Soulbound token issuance based on sentiment + +--- + +### Governance Examples + +#### 4. AI Governance (`contracts/governance/AIGovernance.sol`) + +A DAO governance system with AI-powered proposal analysis and Q&A. + +**Features:** +- Automatic proposal summarization using LLM +- AI-generated impact analysis (benefits & risks) +- Interactive Q&A about proposals +- Standard voting mechanics (For/Against/Abstain) +- Quorum and execution logic + +**Architecture:** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Proposal │────▢│ LLM Summary │────▢│ LLM Impact β”‚ +β”‚ Created β”‚ β”‚ Generation β”‚ β”‚ Analysis β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Execute │◀────│ Voting Period │◀────│ AI-Enhanced β”‚ +β”‚ if Passed β”‚ β”‚ (For/Against) β”‚ β”‚ Proposal β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Interactive Q&A (Ask AI about Proposal)β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Use Cases:** +- DAO governance (MakerDAO, Compound Governance style) +- Corporate shareholder voting +- Community decision-making with AI assistance + +--- + +## Quick Start + +### Prerequisites + +- Foundry or Hardhat for Solidity development +- Access to OpenGradient testnet +- [solid-ml](https://github.com/OpenGradient/solid-ml) library + +### Installation + +```bash +# Clone the repository +git clone https://github.com/OpenGradient/opengradient-examples.git +cd opengradient-examples + +# Install dependencies (Foundry) +forge install OpenGradient/solid-ml + +# Or with npm/yarn +npm install @opengradient/solid-ml +``` + +### Deployment + +```bash +# Set environment variables +export RPC_URL=https://og-testnet.rpc.url +export PRIVATE_KEY=your_private_key + +# Deploy DynamicFeeAMM +forge create contracts/defi/DynamicFeeAMM.sol:DynamicFeeAMM \ + --constructor-args \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY + +# Deploy LendingRiskAssessor +forge create contracts/defi/LendingRiskAssessor.sol:LendingRiskAssessor \ + --constructor-args \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY + +# Deploy SentimentGatedMint +forge create contracts/nft/SentimentGatedMint.sol:SentimentGatedMint \ + --constructor-args "Collection Name" "SYMBOL" "ipfs://base-uri/" \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY + +# Deploy AIGovernance +forge create contracts/governance/AIGovernance.sol:AIGovernance \ + --rpc-url $RPC_URL \ + --private-key $PRIVATE_KEY +``` + +## OpenGradient Interfaces Used + +| Interface | Description | Examples Using It | +|-----------|-------------|-------------------| +| `OGInference` | ML model and LLM inference | All examples | +| `OGHistorical` | Historical price/data feeds | DynamicFeeAMM | +| `OGPreprocessing` | Statistical operations | DynamicFeeAMM, LendingRiskAssessor | +| `TensorLib` | Tensor data structures | All ML examples | + +## Inference Modes + +| Mode | Description | Use Case | +|------|-------------|----------| +| `TEE` | Trusted Execution Environment | Most secure, tamper-proof | +| `ZK` | Zero-Knowledge Proofs | Verifiable without revealing inputs | +| `VANILLA` | Standard inference | Development/testing | + +## Best Practices + +1. **Model Selection**: Choose appropriate models for your use case + - LLMs for text analysis and generation + - Numerical models for predictions and scoring + +2. **Error Handling**: Always handle inference failures gracefully + - Implement fallback values + - Use try-catch patterns + +3. **Gas Optimization**: + - Cache inference results when possible + - Batch operations where applicable + +4. **Security**: + - Use TEE mode for production deployments + - Validate all inputs before inference + - Implement access controls + +## Resources + +- [OpenGradient Documentation](https://docs.opengradient.ai/) +- [SolidML Library](https://github.com/OpenGradient/solid-ml) +- [OpenGradient SDK](https://github.com/OpenGradient/OpenGradient-SDK) ## Community - * Read our [Documentation](https://docs.opengradient.ai/) - * Join us on [Discord](https://discord.gg/axammqTRDz) - * Follow us on [Twitter](https://x.com/OpenGradient) - * Read our [Blogs](https://opengradient.ai/blog) \ No newline at end of file + +- [Discord](https://discord.gg/axammqTRDz) +- [Twitter](https://x.com/OpenGradient) +- [Blog](https://opengradient.ai/blog) + +## Contributing + +We welcome contributions! Please see our contributing guidelines and submit PRs for: +- New example contracts +- Improved documentation +- Bug fixes + +## License + +MIT License - see [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/contracts/defi/DynamicFeeAMM.sol b/contracts/defi/DynamicFeeAMM.sol new file mode 100644 index 0000000..9ed0c7a --- /dev/null +++ b/contracts/defi/DynamicFeeAMM.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "opengradient-solidml/src/interfaces/OGInference.sol"; +import "opengradient-solidml/src/interfaces/OGHistorical.sol"; +import "opengradient-solidml/src/interfaces/OGPreprocessing.sol"; +import "opengradient-solidml/src/lib/TensorLib.sol"; + +/** + * @title DynamicFeeAMM + * @author OpenGradient Examples + * @notice An AMM that uses ML-predicted volatility to dynamically adjust swap fees. + * @dev This example demonstrates how to: + * 1. Query historical price data using OGHistorical + * 2. Run ML model inference to predict volatility + * 3. Adjust AMM fees based on predictions + * + * Use Cases: + * - Higher fees during volatile periods to protect LPs from impermanent loss + * - Lower fees during stable periods to attract more trading volume + * - MEV protection through unpredictable fee adjustments + */ +contract DynamicFeeAMM { + using TensorLib for TensorLib.MultiDimensionalNumberTensor; + + // ============ Constants ============ + + /// @notice Minimum fee (0.01% = 1 basis point) + uint256 public constant MIN_FEE_BPS = 1; + + /// @notice Maximum fee (1% = 100 basis points) + uint256 public constant MAX_FEE_BPS = 100; + + /// @notice Base fee when no volatility prediction is available (0.3%) + uint256 public constant BASE_FEE_BPS = 30; + + /// @notice Number of price points to use for volatility prediction + uint32 public constant LOOKBACK_CANDLES = 24; + + /// @notice Candle duration in minutes (1 hour candles) + uint32 public constant CANDLE_DURATION = 60; + + // ============ State Variables ============ + + /// @notice Model CID for volatility prediction (placeholder - replace with actual model) + string public volatilityModelCID; + + /// @notice Token pair being traded + string public baseToken; + string public quoteToken; + + /// @notice Current dynamic fee in basis points + uint256 public currentFeeBps; + + /// @notice Last predicted volatility score (0-100) + TensorLib.Number public lastVolatilityScore; + + /// @notice Timestamp of last fee update + uint256 public lastFeeUpdate; + + /// @notice Owner of the contract + address public owner; + + // ============ Events ============ + + event FeeUpdated(uint256 oldFeeBps, uint256 newFeeBps, int128 volatilityScore); + event Swap(address indexed user, uint256 amountIn, uint256 amountOut, uint256 feePaid); + event ModelUpdated(string oldModelCID, string newModelCID); + + // ============ Errors ============ + + error Unauthorized(); + error InvalidModelCID(); + error InvalidTokenPair(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + // ============ Constructor ============ + + /** + * @notice Initialize the Dynamic Fee AMM + * @param _volatilityModelCID CID of the volatility prediction model on Model Hub + * @param _baseToken Base token symbol (e.g., "ETH") + * @param _quoteToken Quote token symbol (e.g., "USDC") + */ + constructor( + string memory _volatilityModelCID, + string memory _baseToken, + string memory _quoteToken + ) { + if (bytes(_volatilityModelCID).length == 0) revert InvalidModelCID(); + if (bytes(_baseToken).length == 0 || bytes(_quoteToken).length == 0) revert InvalidTokenPair(); + + volatilityModelCID = _volatilityModelCID; + baseToken = _baseToken; + quoteToken = _quoteToken; + currentFeeBps = BASE_FEE_BPS; + owner = msg.sender; + } + + // ============ Core Functions ============ + + /** + * @notice Update the swap fee based on ML-predicted volatility + * @dev Fetches historical prices, runs volatility model, and adjusts fee + * @return newFeeBps The newly calculated fee in basis points + */ + function updateFeeFromVolatility() external returns (uint256 newFeeBps) { + // Step 1: Query historical price candles + HistoricalInputQuery memory query = HistoricalInputQuery({ + base: baseToken, + quote: quoteToken, + total_candles: LOOKBACK_CANDLES, + candle_duration_in_mins: CANDLE_DURATION, + order: CandleOrder.Descending, + candle_types: _getDefaultCandleTypes() + }); + + TensorLib.Number[] memory priceData = OG_HISTORICAL_CONTRACT.queryHistoricalCandles(query); + + // Step 2: Calculate statistical features using preprocessing + TensorLib.Number memory priceStdDev = OG_PREPROCESSING_CONTRACT.stdDev(priceData); + TensorLib.Number memory priceMean = OG_PREPROCESSING_CONTRACT.mean(priceData); + + // Step 3: Prepare input for volatility model + // Model expects: [stddev, mean, recent_prices...] + ModelInput memory modelInput = ModelInput( + new TensorLib.MultiDimensionalNumberTensor[](1), + new TensorLib.StringTensor[](0) + ); + + // Create input tensor with statistical features + TensorLib.Number[] memory inputFeatures = new TensorLib.Number[](2 + priceData.length); + inputFeatures[0] = priceStdDev; + inputFeatures[1] = priceMean; + for (uint i = 0; i < priceData.length; i++) { + inputFeatures[2 + i] = priceData[i]; + } + modelInput.numbers[0] = TensorLib.numberTensor1D("input", inputFeatures); + + // Step 4: Run volatility prediction model (with ZK verification) + ModelOutput memory output = OG_INFERENCE_CONTRACT.runModelInference( + ModelInferenceRequest({ + mode: ModelInferenceMode.ZK, + modelCID: volatilityModelCID, + input: modelInput + }) + ); + + // Step 5: Extract volatility score and calculate new fee + if (!output.is_simulation_result && output.numbers.length > 0 && output.numbers[0].values.length > 0) { + lastVolatilityScore = output.numbers[0].values[0]; + newFeeBps = _calculateFeeFromVolatility(lastVolatilityScore); + } else { + // Fallback to base fee if model fails + newFeeBps = BASE_FEE_BPS; + } + + // Step 6: Update state + uint256 oldFeeBps = currentFeeBps; + currentFeeBps = newFeeBps; + lastFeeUpdate = block.timestamp; + + emit FeeUpdated(oldFeeBps, newFeeBps, lastVolatilityScore.value); + + return newFeeBps; + } + + /** + * @notice Execute a swap with dynamic fee + * @dev For demo purposes - actual AMM logic would be more complex + * @param amountIn Amount of tokens to swap in + * @return amountOut Amount of tokens received after fee + */ + function swap(uint256 amountIn) external returns (uint256 amountOut) { + // Calculate fee + uint256 feePaid = (amountIn * currentFeeBps) / 10000; + amountOut = amountIn - feePaid; + + // In production, implement actual AMM swap logic here + // This is simplified for demonstration + + emit Swap(msg.sender, amountIn, amountOut, feePaid); + + return amountOut; + } + + /** + * @notice Get the current swap fee percentage + * @return Fee as a decimal string (e.g., "0.30" for 0.30%) + */ + function getCurrentFeePercent() external view returns (string memory) { + // Returns fee as percentage (currentFeeBps / 100) + // In production, would use proper decimal formatting + return "Dynamic fee based on volatility"; + } + + // ============ Admin Functions ============ + + /** + * @notice Update the volatility prediction model + * @param newModelCID New model CID from Model Hub + */ + function setVolatilityModel(string memory newModelCID) external onlyOwner { + if (bytes(newModelCID).length == 0) revert InvalidModelCID(); + + string memory oldModelCID = volatilityModelCID; + volatilityModelCID = newModelCID; + + emit ModelUpdated(oldModelCID, newModelCID); + } + + // ============ Internal Functions ============ + + /** + * @notice Calculate fee from volatility score + * @dev Maps volatility score (0-100) to fee range (MIN_FEE_BPS to MAX_FEE_BPS) + * @param volatility Predicted volatility score + * @return Fee in basis points + */ + function _calculateFeeFromVolatility(TensorLib.Number memory volatility) internal pure returns (uint256) { + // Convert TensorLib.Number to usable value + // Assuming volatility is 0-100 scale with appropriate decimals + int256 volScore; + if (volatility.decimals > 0) { + volScore = int256(volatility.value) / int256(10 ** uint256(int256(volatility.decimals))); + } else { + volScore = int256(volatility.value); + } + + // Clamp to 0-100 + if (volScore < 0) volScore = 0; + if (volScore > 100) volScore = 100; + + // Linear interpolation: low volatility = low fee, high volatility = high fee + // fee = MIN_FEE + (MAX_FEE - MIN_FEE) * volatility / 100 + uint256 feeRange = MAX_FEE_BPS - MIN_FEE_BPS; + uint256 fee = MIN_FEE_BPS + (feeRange * uint256(volScore)) / 100; + + return fee; + } + + /** + * @notice Get default candle types for price query + * @return Array of candle types (Close prices) + */ + function _getDefaultCandleTypes() internal pure returns (CandleType[] memory) { + CandleType[] memory types = new CandleType[](1); + types[0] = CandleType.Close; + return types; + } +} diff --git a/contracts/defi/LendingRiskAssessor.sol b/contracts/defi/LendingRiskAssessor.sol new file mode 100644 index 0000000..23260a5 --- /dev/null +++ b/contracts/defi/LendingRiskAssessor.sol @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "opengradient-solidml/src/interfaces/OGInference.sol"; +import "opengradient-solidml/src/lib/TensorLib.sol"; + +/** + * @title LendingRiskAssessor + * @author OpenGradient Examples + * @notice A lending protocol risk assessment system using on-chain ML inference. + * @dev This example demonstrates how to: + * 1. Use ML models to assess borrower creditworthiness + * 2. Calculate risk-adjusted interest rates + * 3. Determine collateral requirements based on risk scores + * + * Use Cases: + * - DeFi lending protocols (Aave, Compound style) + * - Under-collateralized lending with reputation scores + * - Dynamic interest rate models + */ +contract LendingRiskAssessor { + using TensorLib for TensorLib.MultiDimensionalNumberTensor; + + // ============ Structs ============ + + struct BorrowerProfile { + uint256 totalBorrowed; // Total amount ever borrowed + uint256 totalRepaid; // Total amount repaid + uint256 currentDebt; // Outstanding debt + uint256 collateralValue; // Current collateral value + uint256 accountAge; // Age of account in days + uint256 liquidationCount; // Number of times liquidated + TensorLib.Number riskScore; // Last calculated risk score (0-100, lower = safer) + uint256 lastAssessment; // Timestamp of last risk assessment + } + + struct LoanTerms { + uint256 maxBorrowAmount; // Maximum borrowable amount + uint256 interestRateBps; // Annual interest rate in basis points + uint256 minCollateralRatioBps; // Minimum collateral ratio in basis points + uint256 validUntil; // Timestamp when terms expire + } + + // ============ Constants ============ + + /// @notice Base interest rate (5% = 500 bps) + uint256 public constant BASE_INTEREST_RATE_BPS = 500; + + /// @notice Maximum interest rate for high-risk borrowers (30% = 3000 bps) + uint256 public constant MAX_INTEREST_RATE_BPS = 3000; + + /// @notice Base collateral ratio (150% = 15000 bps) + uint256 public constant BASE_COLLATERAL_RATIO_BPS = 15000; + + /// @notice Maximum collateral ratio for high-risk borrowers (300% = 30000 bps) + uint256 public constant MAX_COLLATERAL_RATIO_BPS = 30000; + + /// @notice Risk assessment validity period (1 day) + uint256 public constant ASSESSMENT_VALIDITY = 1 days; + + /// @notice Low risk threshold (score 0-30) + int128 public constant LOW_RISK_THRESHOLD = 30; + + /// @notice Medium risk threshold (score 31-60) + int128 public constant MEDIUM_RISK_THRESHOLD = 60; + + // ============ State Variables ============ + + /// @notice Model CID for risk assessment + string public riskModelCID; + + /// @notice Borrower profiles + mapping(address => BorrowerProfile) public borrowerProfiles; + + /// @notice Active loan terms + mapping(address => LoanTerms) public loanTerms; + + /// @notice Protocol owner + address public owner; + + // ============ Events ============ + + event RiskAssessed( + address indexed borrower, + int128 riskScore, + uint256 interestRateBps, + uint256 collateralRatioBps + ); + event LoanTermsGenerated( + address indexed borrower, + uint256 maxBorrowAmount, + uint256 interestRateBps, + uint256 collateralRatioBps + ); + event BorrowerHistoryUpdated( + address indexed borrower, + uint256 totalBorrowed, + uint256 totalRepaid + ); + + // ============ Errors ============ + + error Unauthorized(); + error InvalidModelCID(); + error AssessmentExpired(); + error InsufficientCollateral(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + // ============ Constructor ============ + + /** + * @notice Initialize the Lending Risk Assessor + * @param _riskModelCID CID of the risk assessment model on Model Hub + */ + constructor(string memory _riskModelCID) { + if (bytes(_riskModelCID).length == 0) revert InvalidModelCID(); + riskModelCID = _riskModelCID; + owner = msg.sender; + } + + // ============ Core Functions ============ + + /** + * @notice Assess the risk score for a borrower using ML inference + * @dev Collects borrower metrics and runs them through the risk model + * @param borrower Address of the borrower to assess + * @return riskScore The calculated risk score (0-100, lower = safer) + */ + function assessBorrowerRisk(address borrower) external returns (TensorLib.Number memory riskScore) { + BorrowerProfile storage profile = borrowerProfiles[borrower]; + + // Prepare input features for the risk model + // Features: [repayment_ratio, utilization_ratio, account_age, liquidation_count] + ModelInput memory modelInput = ModelInput( + new TensorLib.MultiDimensionalNumberTensor[](1), + new TensorLib.StringTensor[](0) + ); + + TensorLib.Number[] memory features = new TensorLib.Number[](4); + + // Feature 1: Repayment ratio (repaid / borrowed, scaled by 1e4) + if (profile.totalBorrowed > 0) { + features[0] = TensorLib.Number( + int128(int256((profile.totalRepaid * 10000) / profile.totalBorrowed)), + 4 // 4 decimals + ); + } else { + features[0] = TensorLib.Number(10000, 4); // 100% for new borrowers + } + + // Feature 2: Current utilization ratio (debt / collateral, scaled by 1e4) + if (profile.collateralValue > 0) { + features[1] = TensorLib.Number( + int128(int256((profile.currentDebt * 10000) / profile.collateralValue)), + 4 + ); + } else { + features[1] = TensorLib.Number(10000, 4); // 100% if no collateral + } + + // Feature 3: Account age in days (scaled by 1e2) + features[2] = TensorLib.Number(int128(int256(profile.accountAge * 100)), 2); + + // Feature 4: Liquidation count (raw integer) + features[3] = TensorLib.Number(int128(int256(profile.liquidationCount)), 0); + + modelInput.numbers[0] = TensorLib.numberTensor1D("borrower_features", features); + + // Run the risk assessment model with TEE verification + ModelOutput memory output = OG_INFERENCE_CONTRACT.runModelInference( + ModelInferenceRequest({ + mode: ModelInferenceMode.TEE, + modelCID: riskModelCID, + input: modelInput + }) + ); + + // Extract risk score from model output + if (!output.is_simulation_result && output.numbers.length > 0 && output.numbers[0].values.length > 0) { + riskScore = output.numbers[0].values[0]; + } else { + // Default to medium-high risk if model fails + riskScore = TensorLib.Number(70, 0); + } + + // Update borrower profile + profile.riskScore = riskScore; + profile.lastAssessment = block.timestamp; + + // Calculate and store loan terms based on risk + LoanTerms memory terms = _calculateLoanTerms(borrower, riskScore); + loanTerms[borrower] = terms; + + emit RiskAssessed( + borrower, + riskScore.value, + terms.interestRateBps, + terms.minCollateralRatioBps + ); + + return riskScore; + } + + /** + * @notice Get loan terms for a borrower (must have valid risk assessment) + * @param borrower Address of the borrower + * @return terms The calculated loan terms + */ + function getLoanTerms(address borrower) external view returns (LoanTerms memory terms) { + terms = loanTerms[borrower]; + if (block.timestamp > terms.validUntil) revert AssessmentExpired(); + return terms; + } + + /** + * @notice Check if a loan request is within acceptable risk parameters + * @param borrower Address of the borrower + * @param borrowAmount Amount to borrow + * @param collateralAmount Amount of collateral provided + * @return approved Whether the loan is approved + * @return reason Reason if not approved + */ + function checkLoanEligibility( + address borrower, + uint256 borrowAmount, + uint256 collateralAmount + ) external view returns (bool approved, string memory reason) { + LoanTerms memory terms = loanTerms[borrower]; + + // Check if assessment is still valid + if (block.timestamp > terms.validUntil) { + return (false, "Risk assessment expired"); + } + + // Check borrow amount + if (borrowAmount > terms.maxBorrowAmount) { + return (false, "Exceeds maximum borrow amount"); + } + + // Check collateral ratio + uint256 requiredCollateral = (borrowAmount * terms.minCollateralRatioBps) / 10000; + if (collateralAmount < requiredCollateral) { + return (false, "Insufficient collateral"); + } + + return (true, "Loan approved"); + } + + /** + * @notice Update borrower history after loan activity + * @param borrower Address of the borrower + * @param borrowed Amount newly borrowed + * @param repaid Amount repaid + */ + function updateBorrowerHistory( + address borrower, + uint256 borrowed, + uint256 repaid + ) external onlyOwner { + BorrowerProfile storage profile = borrowerProfiles[borrower]; + + profile.totalBorrowed += borrowed; + profile.totalRepaid += repaid; + profile.currentDebt = profile.currentDebt + borrowed - repaid; + + if (profile.accountAge == 0) { + profile.accountAge = 1; // New account + } + + emit BorrowerHistoryUpdated(borrower, profile.totalBorrowed, profile.totalRepaid); + } + + /** + * @notice Update collateral value for a borrower + * @param borrower Address of the borrower + * @param newCollateralValue New collateral value + */ + function updateCollateral(address borrower, uint256 newCollateralValue) external onlyOwner { + borrowerProfiles[borrower].collateralValue = newCollateralValue; + } + + /** + * @notice Record a liquidation event + * @param borrower Address of the liquidated borrower + */ + function recordLiquidation(address borrower) external onlyOwner { + borrowerProfiles[borrower].liquidationCount++; + } + + // ============ Admin Functions ============ + + /** + * @notice Update the risk assessment model + * @param newModelCID New model CID + */ + function setRiskModel(string memory newModelCID) external onlyOwner { + if (bytes(newModelCID).length == 0) revert InvalidModelCID(); + riskModelCID = newModelCID; + } + + // ============ Internal Functions ============ + + /** + * @notice Calculate loan terms based on risk score + * @param borrower Address of the borrower + * @param riskScore Calculated risk score + * @return terms Calculated loan terms + */ + function _calculateLoanTerms( + address borrower, + TensorLib.Number memory riskScore + ) internal view returns (LoanTerms memory terms) { + BorrowerProfile storage profile = borrowerProfiles[borrower]; + + // Convert risk score to integer (0-100) + int256 score = int256(riskScore.value); + if (riskScore.decimals > 0) { + score = score / int256(10 ** uint256(int256(riskScore.decimals))); + } + if (score < 0) score = 0; + if (score > 100) score = 100; + + // Calculate interest rate: higher risk = higher rate + // Linear interpolation from BASE to MAX based on risk score + uint256 rateRange = MAX_INTEREST_RATE_BPS - BASE_INTEREST_RATE_BPS; + uint256 interestRate = BASE_INTEREST_RATE_BPS + (rateRange * uint256(score)) / 100; + + // Calculate collateral ratio: higher risk = higher collateral required + uint256 collateralRange = MAX_COLLATERAL_RATIO_BPS - BASE_COLLATERAL_RATIO_BPS; + uint256 collateralRatio = BASE_COLLATERAL_RATIO_BPS + (collateralRange * uint256(score)) / 100; + + // Calculate max borrow amount based on collateral and risk + uint256 maxBorrow = 0; + if (profile.collateralValue > 0 && collateralRatio > 0) { + maxBorrow = (profile.collateralValue * 10000) / collateralRatio; + } + + terms = LoanTerms({ + maxBorrowAmount: maxBorrow, + interestRateBps: interestRate, + minCollateralRatioBps: collateralRatio, + validUntil: block.timestamp + ASSESSMENT_VALIDITY + }); + + emit LoanTermsGenerated(borrower, maxBorrow, interestRate, collateralRatio); + + return terms; + } + + /** + * @notice Get risk category from score + * @param score Risk score + * @return category Risk category string + */ + function getRiskCategory(TensorLib.Number memory score) external pure returns (string memory category) { + int256 s = int256(score.value); + if (score.decimals > 0) { + s = s / int256(10 ** uint256(int256(score.decimals))); + } + + if (s <= LOW_RISK_THRESHOLD) { + return "LOW_RISK"; + } else if (s <= MEDIUM_RISK_THRESHOLD) { + return "MEDIUM_RISK"; + } else { + return "HIGH_RISK"; + } + } +} diff --git a/contracts/governance/AIGovernance.sol b/contracts/governance/AIGovernance.sol new file mode 100644 index 0000000..5be41f9 --- /dev/null +++ b/contracts/governance/AIGovernance.sol @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "opengradient-solidml/src/interfaces/OGInference.sol"; +import "opengradient-solidml/src/lib/TensorLib.sol"; + +/** + * @title AIGovernance + * @author OpenGradient Examples + * @notice A DAO governance system with AI-powered proposal analysis and summarization. + * @dev This example demonstrates how to: + * 1. Use LLM inference for proposal summarization + * 2. AI-assisted impact analysis for governance decisions + * 3. Multi-turn conversations for interactive proposal Q&A + * + * Use Cases: + * - DAOs (MakerDAO, Compound, Uniswap governance style) + * - Corporate shareholder voting + * - Community decision making + */ +contract AIGovernance { + // ============ Structs ============ + + struct Proposal { + uint256 id; + address proposer; + string title; + string description; + string aiSummary; + string aiImpactAnalysis; + uint256 forVotes; + uint256 againstVotes; + uint256 abstainVotes; + uint256 startTime; + uint256 endTime; + ProposalState state; + bool aiAnalyzed; + } + + struct Vote { + bool hasVoted; + VoteType voteType; + uint256 weight; + } + + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Executed + } + + enum VoteType { + Against, + For, + Abstain + } + + // ============ Constants ============ + + /// @notice Model ID for LLM inference + string public constant LLM_MODEL_ID = "meta-llama/Meta-Llama-3-8B-Instruct"; + + /// @notice Maximum tokens for LLM response + uint32 public constant MAX_TOKENS = 500; + + /// @notice Voting period duration (3 days) + uint256 public constant VOTING_PERIOD = 3 days; + + /// @notice Delay before voting starts (1 day) + uint256 public constant VOTING_DELAY = 1 days; + + /// @notice Quorum percentage (4% of total supply) + uint256 public constant QUORUM_PERCENTAGE = 4; + + // ============ State Variables ============ + + /// @notice Total proposals created + uint256 public proposalCount; + + /// @notice All proposals + mapping(uint256 => Proposal) public proposals; + + /// @notice Votes by proposal and voter + mapping(uint256 => mapping(address => Vote)) public votes; + + /// @notice Voting power of each address + mapping(address => uint256) public votingPower; + + /// @notice Total voting power + uint256 public totalVotingPower; + + /// @notice Q&A interaction history per proposal + mapping(uint256 => string[]) public proposalQA; + + /// @notice Protocol owner + address public owner; + + // ============ Events ============ + + event ProposalCreated( + uint256 indexed proposalId, + address indexed proposer, + string title + ); + event AIAnalysisCompleted( + uint256 indexed proposalId, + string summary, + string impactAnalysis + ); + event VoteCast( + uint256 indexed proposalId, + address indexed voter, + VoteType voteType, + uint256 weight + ); + event ProposalExecuted(uint256 indexed proposalId); + event QuestionAnswered(uint256 indexed proposalId, string question, string answer); + event VotingPowerUpdated(address indexed account, uint256 newPower); + + // ============ Errors ============ + + error Unauthorized(); + error InvalidProposal(); + error ProposalNotActive(); + error AlreadyVoted(); + error NoVotingPower(); + error VotingNotEnded(); + error ProposalNotSucceeded(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + modifier validProposal(uint256 proposalId) { + if (proposalId >= proposalCount) revert InvalidProposal(); + _; + } + + // ============ Constructor ============ + + constructor() { + owner = msg.sender; + } + + // ============ Core Functions ============ + + /** + * @notice Create a new governance proposal + * @param title Short title of the proposal + * @param description Full description of the proposal + * @return proposalId The ID of the created proposal + */ + function createProposal( + string memory title, + string memory description + ) external returns (uint256 proposalId) { + proposalId = proposalCount; + proposalCount++; + + proposals[proposalId] = Proposal({ + id: proposalId, + proposer: msg.sender, + title: title, + description: description, + aiSummary: "", + aiImpactAnalysis: "", + forVotes: 0, + againstVotes: 0, + abstainVotes: 0, + startTime: block.timestamp + VOTING_DELAY, + endTime: block.timestamp + VOTING_DELAY + VOTING_PERIOD, + state: ProposalState.Pending, + aiAnalyzed: false + }); + + emit ProposalCreated(proposalId, msg.sender, title); + + // Automatically trigger AI analysis + _analyzeProposal(proposalId); + + return proposalId; + } + + /** + * @notice Cast a vote on a proposal + * @param proposalId ID of the proposal + * @param voteType Type of vote (For, Against, Abstain) + */ + function castVote(uint256 proposalId, VoteType voteType) external validProposal(proposalId) { + Proposal storage proposal = proposals[proposalId]; + + // Check voting is active + if (block.timestamp < proposal.startTime) { + proposal.state = ProposalState.Pending; + revert ProposalNotActive(); + } + if (block.timestamp > proposal.endTime) { + _updateProposalState(proposalId); + revert ProposalNotActive(); + } + proposal.state = ProposalState.Active; + + // Check voter hasn't voted + Vote storage vote = votes[proposalId][msg.sender]; + if (vote.hasVoted) revert AlreadyVoted(); + + // Check voting power + uint256 weight = votingPower[msg.sender]; + if (weight == 0) revert NoVotingPower(); + + // Record vote + vote.hasVoted = true; + vote.voteType = voteType; + vote.weight = weight; + + // Count vote + if (voteType == VoteType.For) { + proposal.forVotes += weight; + } else if (voteType == VoteType.Against) { + proposal.againstVotes += weight; + } else { + proposal.abstainVotes += weight; + } + + emit VoteCast(proposalId, msg.sender, voteType, weight); + } + + /** + * @notice Ask the AI a question about a proposal + * @param proposalId ID of the proposal + * @param question Question to ask + * @return answer AI-generated answer + */ + function askAboutProposal( + uint256 proposalId, + string memory question + ) external validProposal(proposalId) returns (string memory answer) { + Proposal storage proposal = proposals[proposalId]; + + // Build the context and question prompt + string memory prompt = string(abi.encodePacked( + "You are a governance analyst assistant. Based on the following proposal, answer the question clearly and concisely.\n\n", + "PROPOSAL TITLE: ", proposal.title, "\n\n", + "PROPOSAL DESCRIPTION: ", proposal.description, "\n\n", + "AI SUMMARY: ", proposal.aiSummary, "\n\n", + "IMPACT ANALYSIS: ", proposal.aiImpactAnalysis, "\n\n", + "QUESTION: ", question, "\n\n", + "Please provide a helpful, factual answer based only on the proposal content." + )); + + // Make LLM inference call + answer = _runLLMChat(prompt); + + // Store Q&A for transparency + string memory qa = string(abi.encodePacked("Q: ", question, "\nA: ", answer)); + proposalQA[proposalId].push(qa); + + emit QuestionAnswered(proposalId, question, answer); + + return answer; + } + + /** + * @notice Execute a succeeded proposal + * @param proposalId ID of the proposal to execute + */ + function executeProposal(uint256 proposalId) external validProposal(proposalId) { + Proposal storage proposal = proposals[proposalId]; + + if (block.timestamp <= proposal.endTime) revert VotingNotEnded(); + + _updateProposalState(proposalId); + + if (proposal.state != ProposalState.Succeeded) revert ProposalNotSucceeded(); + + proposal.state = ProposalState.Executed; + + // In production, this would execute the proposal's actions + // For demo purposes, we just emit an event + + emit ProposalExecuted(proposalId); + } + + /** + * @notice Get proposal summary and analysis + * @param proposalId ID of the proposal + * @return summary AI-generated summary + * @return impactAnalysis AI-generated impact analysis + */ + function getProposalAnalysis(uint256 proposalId) + external + view + validProposal(proposalId) + returns (string memory summary, string memory impactAnalysis) + { + Proposal storage proposal = proposals[proposalId]; + return (proposal.aiSummary, proposal.aiImpactAnalysis); + } + + /** + * @notice Get Q&A history for a proposal + * @param proposalId ID of the proposal + * @return qaHistory Array of Q&A strings + */ + function getProposalQA(uint256 proposalId) + external + view + validProposal(proposalId) + returns (string[] memory qaHistory) + { + return proposalQA[proposalId]; + } + + /** + * @notice Get current state of a proposal + * @param proposalId ID of the proposal + * @return state Current proposal state + */ + function getProposalState(uint256 proposalId) + external + view + validProposal(proposalId) + returns (ProposalState state) + { + Proposal storage proposal = proposals[proposalId]; + + if (proposal.state == ProposalState.Canceled || + proposal.state == ProposalState.Executed) { + return proposal.state; + } + + if (block.timestamp < proposal.startTime) { + return ProposalState.Pending; + } + + if (block.timestamp <= proposal.endTime) { + return ProposalState.Active; + } + + // Voting ended - determine outcome + uint256 totalVotes = proposal.forVotes + proposal.againstVotes + proposal.abstainVotes; + uint256 quorum = (totalVotingPower * QUORUM_PERCENTAGE) / 100; + + if (totalVotes < quorum) { + return ProposalState.Defeated; + } + + if (proposal.forVotes > proposal.againstVotes) { + return ProposalState.Succeeded; + } + + return ProposalState.Defeated; + } + + // ============ Admin Functions ============ + + /** + * @notice Update voting power for an address + * @param account Address to update + * @param power New voting power + */ + function setVotingPower(address account, uint256 power) external onlyOwner { + uint256 oldPower = votingPower[account]; + votingPower[account] = power; + + // Update total + totalVotingPower = totalVotingPower - oldPower + power; + + emit VotingPowerUpdated(account, power); + } + + /** + * @notice Cancel a proposal (owner only) + * @param proposalId ID of the proposal to cancel + */ + function cancelProposal(uint256 proposalId) external onlyOwner validProposal(proposalId) { + proposals[proposalId].state = ProposalState.Canceled; + } + + // ============ Internal Functions ============ + + /** + * @notice Analyze a proposal using LLM + * @param proposalId ID of the proposal to analyze + */ + function _analyzeProposal(uint256 proposalId) internal { + Proposal storage proposal = proposals[proposalId]; + + // Generate summary + string memory summaryPrompt = string(abi.encodePacked( + "Summarize this governance proposal in 2-3 sentences. Be factual and neutral.\n\n", + "Title: ", proposal.title, "\n\n", + "Description: ", proposal.description + )); + proposal.aiSummary = _runLLMChat(summaryPrompt); + + // Generate impact analysis + string memory impactPrompt = string(abi.encodePacked( + "Analyze the potential impacts of this governance proposal. List 2-3 key points about potential benefits and risks. Be balanced and factual.\n\n", + "Title: ", proposal.title, "\n\n", + "Description: ", proposal.description + )); + proposal.aiImpactAnalysis = _runLLMChat(impactPrompt); + + proposal.aiAnalyzed = true; + + emit AIAnalysisCompleted(proposalId, proposal.aiSummary, proposal.aiImpactAnalysis); + } + + /** + * @notice Run LLM chat inference + * @param prompt The prompt to send to the LLM + * @return response The LLM response + */ + function _runLLMChat(string memory prompt) internal returns (string memory response) { + ChatMessage[] memory messages = new ChatMessage[](1); + messages[0] = ChatMessage({ + role: "user", + content: prompt, + name: "", + tool_call_id: "", + tool_calls: new ToolCall[](0) + }); + + string[] memory stopSequence = new string[](0); + + LLMChatResponse memory llmResponse = OG_INFERENCE_CONTRACT.runLLMChat( + LLMChatRequest({ + mode: LLMInferenceMode.TEE, + modelCID: LLM_MODEL_ID, + messages: messages, + tools: new ToolDefinition[](0), + tool_choice: "", + max_tokens: MAX_TOKENS, + stop_sequence: stopSequence, + temperature: 30 // Slightly creative but mostly factual + }) + ); + + return llmResponse.message.content; + } + + /** + * @notice Update proposal state based on votes + * @param proposalId ID of the proposal + */ + function _updateProposalState(uint256 proposalId) internal { + Proposal storage proposal = proposals[proposalId]; + + if (proposal.state == ProposalState.Canceled || + proposal.state == ProposalState.Executed) { + return; + } + + if (block.timestamp <= proposal.endTime) { + proposal.state = ProposalState.Active; + return; + } + + // Voting ended - determine outcome + uint256 totalVotes = proposal.forVotes + proposal.againstVotes + proposal.abstainVotes; + uint256 quorum = (totalVotingPower * QUORUM_PERCENTAGE) / 100; + + if (totalVotes < quorum) { + proposal.state = ProposalState.Defeated; + return; + } + + if (proposal.forVotes > proposal.againstVotes) { + proposal.state = ProposalState.Succeeded; + } else { + proposal.state = ProposalState.Defeated; + } + } +} diff --git a/contracts/nft/SentimentGatedMint.sol b/contracts/nft/SentimentGatedMint.sol new file mode 100644 index 0000000..ecf9481 --- /dev/null +++ b/contracts/nft/SentimentGatedMint.sol @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "opengradient-solidml/src/interfaces/OGInference.sol"; +import "opengradient-solidml/src/lib/TensorLib.sol"; + +/** + * @title SentimentGatedMint + * @author OpenGradient Examples + * @notice An NFT collection where minting is gated by LLM sentiment analysis. + * @dev This example demonstrates how to: + * 1. Use LLM inference for text sentiment analysis + * 2. Gate smart contract actions based on LLM output + * 3. Build AI-powered access control mechanisms + * + * Use Cases: + * - Community-driven content moderation + * - AI-curated NFT collections + * - Sentiment-based token gating + */ +contract SentimentGatedMint { + // ============ Structs ============ + + struct MintRequest { + address requester; + string userMessage; + string analysisResult; + bool approved; + uint256 timestamp; + } + + struct NFTMetadata { + string name; + string userMessage; + string sentimentAnalysis; + uint256 mintedAt; + } + + // ============ Constants ============ + + /// @notice Model ID for sentiment analysis (Meta Llama 3 or similar) + string public constant LLM_MODEL_ID = "meta-llama/Meta-Llama-3-8B-Instruct"; + + /// @notice Maximum tokens for LLM response + uint32 public constant MAX_TOKENS = 200; + + /// @notice Collection name + string public constant NAME = "Positive Vibes Collection"; + + /// @notice Collection symbol + string public constant SYMBOL = "VIBES"; + + // ============ State Variables ============ + + /// @notice Total supply of minted NFTs + uint256 public totalSupply; + + /// @notice NFT owners + mapping(uint256 => address) public ownerOf; + + /// @notice NFT metadata + mapping(uint256 => NFTMetadata) public tokenMetadata; + + /// @notice Balance of each address + mapping(address => uint256) public balanceOf; + + /// @notice Mint request history + mapping(address => MintRequest[]) public mintRequests; + + /// @notice Whether an address has an active pending request + mapping(address => bool) public hasPendingRequest; + + /// @notice Protocol owner + address public owner; + + // ============ Events ============ + + event MintRequested(address indexed requester, string userMessage); + event SentimentAnalyzed(address indexed requester, string result, bool approved); + event NFTMinted(address indexed to, uint256 indexed tokenId, string name); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + // ============ Errors ============ + + error Unauthorized(); + error EmptyMessage(); + error PendingRequestExists(); + error NoApprovedRequest(); + error InvalidTokenId(); + + // ============ Modifiers ============ + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + // ============ Constructor ============ + + constructor() { + owner = msg.sender; + } + + // ============ Core Functions ============ + + /** + * @notice Request to mint an NFT with a positive message + * @dev The message will be analyzed for sentiment before minting is allowed + * @param userMessage A positive message to include with the NFT + */ + function requestMint(string memory userMessage) external { + if (bytes(userMessage).length == 0) revert EmptyMessage(); + if (hasPendingRequest[msg.sender]) revert PendingRequestExists(); + + // Create mint request + mintRequests[msg.sender].push(MintRequest({ + requester: msg.sender, + userMessage: userMessage, + analysisResult: "", + approved: false, + timestamp: block.timestamp + })); + hasPendingRequest[msg.sender] = true; + + emit MintRequested(msg.sender, userMessage); + + // Analyze sentiment using LLM + _analyzeSentiment(msg.sender, userMessage); + } + + /** + * @notice Complete the mint if the sentiment analysis was positive + * @param nftName Name for the NFT + */ + function completeMint(string memory nftName) external { + MintRequest[] storage requests = mintRequests[msg.sender]; + if (requests.length == 0) revert NoApprovedRequest(); + + MintRequest storage latestRequest = requests[requests.length - 1]; + if (!latestRequest.approved) revert NoApprovedRequest(); + + // Mint the NFT + uint256 tokenId = totalSupply; + totalSupply++; + + ownerOf[tokenId] = msg.sender; + balanceOf[msg.sender]++; + + tokenMetadata[tokenId] = NFTMetadata({ + name: nftName, + userMessage: latestRequest.userMessage, + sentimentAnalysis: latestRequest.analysisResult, + mintedAt: block.timestamp + }); + + // Reset pending status + hasPendingRequest[msg.sender] = false; + + emit NFTMinted(msg.sender, tokenId, nftName); + emit Transfer(address(0), msg.sender, tokenId); + } + + /** + * @notice Get the latest mint request for an address + * @param requester Address to check + * @return request The latest mint request + */ + function getLatestRequest(address requester) external view returns (MintRequest memory request) { + MintRequest[] storage requests = mintRequests[requester]; + if (requests.length == 0) { + return MintRequest({ + requester: requester, + userMessage: "", + analysisResult: "", + approved: false, + timestamp: 0 + }); + } + return requests[requests.length - 1]; + } + + /** + * @notice Get NFT metadata + * @param tokenId Token ID to query + * @return metadata NFT metadata + */ + function getTokenMetadata(uint256 tokenId) external view returns (NFTMetadata memory metadata) { + if (tokenId >= totalSupply) revert InvalidTokenId(); + return tokenMetadata[tokenId]; + } + + /** + * @notice Simple transfer function + * @param to Recipient address + * @param tokenId Token ID to transfer + */ + function transfer(address to, uint256 tokenId) external { + if (ownerOf[tokenId] != msg.sender) revert Unauthorized(); + if (tokenId >= totalSupply) revert InvalidTokenId(); + + ownerOf[tokenId] = to; + balanceOf[msg.sender]--; + balanceOf[to]++; + + emit Transfer(msg.sender, to, tokenId); + } + + // ============ Internal Functions ============ + + /** + * @notice Analyze the sentiment of a user's message using LLM + * @dev Uses TEE-secured LLM inference for verifiable analysis + * @param requester Address of the requester + * @param userMessage Message to analyze + */ + function _analyzeSentiment(address requester, string memory userMessage) internal { + // Build the analysis prompt + string memory systemPrompt = string(abi.encodePacked( + "You are a sentiment analyzer. Analyze if the following message is positive and appropriate for an NFT collection celebrating positivity. ", + "Reply with ONLY one of these exact words: APPROVED (if positive/uplifting) or REJECTED (if negative/inappropriate). ", + "Do not include any other text.\n\n", + "Message to analyze: ", + userMessage + )); + + // Prepare LLM chat request + ChatMessage[] memory messages = new ChatMessage[](1); + messages[0] = ChatMessage({ + role: "user", + content: systemPrompt, + name: "", + tool_call_id: "", + tool_calls: new ToolCall[](0) + }); + + string[] memory stopSequence = new string[](2); + stopSequence[0] = "\n"; + stopSequence[1] = "."; + + // Make LLM inference call with TEE verification + LLMChatResponse memory response = OG_INFERENCE_CONTRACT.runLLMChat( + LLMChatRequest({ + mode: LLMInferenceMode.TEE, + modelCID: LLM_MODEL_ID, + messages: messages, + tools: new ToolDefinition[](0), + tool_choice: "", + max_tokens: MAX_TOKENS, + stop_sequence: stopSequence, + temperature: 0 // Deterministic for consistent results + }) + ); + + // Parse the response + string memory analysisResult = response.message.content; + bool approved = _isApproved(analysisResult); + + // Update the request + MintRequest[] storage requests = mintRequests[requester]; + MintRequest storage latestRequest = requests[requests.length - 1]; + latestRequest.analysisResult = analysisResult; + latestRequest.approved = approved; + + emit SentimentAnalyzed(requester, analysisResult, approved); + } + + /** + * @notice Check if the LLM response indicates approval + * @param response LLM response text + * @return approved Whether the sentiment is positive + */ + function _isApproved(string memory response) internal pure returns (bool approved) { + // Simple check for "APPROVED" in the response + bytes memory responseBytes = bytes(response); + bytes memory approvedBytes = bytes("APPROVED"); + + if (responseBytes.length < approvedBytes.length) { + return false; + } + + // Check if response starts with or contains "APPROVED" + for (uint i = 0; i <= responseBytes.length - approvedBytes.length; i++) { + bool found = true; + for (uint j = 0; j < approvedBytes.length; j++) { + // Case-insensitive comparison + bytes1 respChar = responseBytes[i + j]; + bytes1 appChar = approvedBytes[j]; + + // Convert to uppercase for comparison + if (respChar >= 0x61 && respChar <= 0x7A) { + respChar = bytes1(uint8(respChar) - 32); + } + if (appChar >= 0x61 && appChar <= 0x7A) { + appChar = bytes1(uint8(appChar) - 32); + } + + if (respChar != appChar) { + found = false; + break; + } + } + if (found) { + return true; + } + } + + return false; + } + + // ============ Admin Functions ============ + + /** + * @notice Emergency override for mint approval (owner only) + * @param requester Address to approve + */ + function adminApprove(address requester) external onlyOwner { + MintRequest[] storage requests = mintRequests[requester]; + if (requests.length == 0) revert NoApprovedRequest(); + + MintRequest storage latestRequest = requests[requests.length - 1]; + latestRequest.approved = true; + latestRequest.analysisResult = "ADMIN_APPROVED"; + + emit SentimentAnalyzed(requester, "ADMIN_APPROVED", true); + } + + /** + * @notice Reset pending request status (owner only) + * @param requester Address to reset + */ + function resetPending(address requester) external onlyOwner { + hasPendingRequest[requester] = false; + } +}