|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import "@pythnetwork/entropy-sdk-solidity/IEntropyV2.sol"; |
| 5 | +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; |
| 6 | +import "@pythnetwork/entropy-sdk-solidity/EntropyStructsV2.sol"; |
| 7 | + |
| 8 | +contract CreditScore is IEntropyConsumer { |
| 9 | + // Structs to hold Polymarket data |
| 10 | + struct ClosedPosition { |
| 11 | + int256 realizedPnl; |
| 12 | + uint256 totalBought; |
| 13 | + bytes32 asset; |
| 14 | + } |
| 15 | + |
| 16 | + struct CurrentPosition { |
| 17 | + uint256 size; |
| 18 | + uint256 avgPrice; |
| 19 | + int256 initialValue; |
| 20 | + int256 currentValue; |
| 21 | + int256 cashPnl; |
| 22 | + int256 percentPnl; |
| 23 | + uint256 totalBought; |
| 24 | + int256 realizedPnl; |
| 25 | + int256 percentRealizedPnl; |
| 26 | + uint256 curPrice; |
| 27 | + } |
| 28 | + |
| 29 | + struct UserData { |
| 30 | + string name; |
| 31 | + uint256 value; |
| 32 | + ClosedPosition[] closedPositions; |
| 33 | + CurrentPosition[] currentPositions; |
| 34 | + } |
| 35 | + |
| 36 | + struct CreditScoreData { |
| 37 | + uint256 score; |
| 38 | + uint256 timestamp; |
| 39 | + bytes32 entropyUsed; |
| 40 | + bool isCalculated; |
| 41 | + } |
| 42 | + |
| 43 | + // Events |
| 44 | + event CreditScoreRequested(address indexed user, uint64 sequenceNumber); |
| 45 | + event CreditScoreCalculated(address indexed user, uint256 score, bytes32 entropyUsed); |
| 46 | + |
| 47 | + // State variables |
| 48 | + IEntropyV2 private entropy; |
| 49 | + address private entropyProvider; |
| 50 | + |
| 51 | + mapping(address => UserData) public userData; |
| 52 | + mapping(address => CreditScoreData) public creditScores; |
| 53 | + mapping(uint64 => address) private sequenceToUser; |
| 54 | + mapping(address => uint64) private userToSequence; |
| 55 | + |
| 56 | + // Constants for score calculation |
| 57 | + uint256 private constant MAX_SCORE = 999; |
| 58 | + uint256 private constant MIN_SCORE = 0; |
| 59 | + uint256 private constant ENTROPY_VARIANCE_WEIGHT = 50; // Max 50 points of variance from entropy |
| 60 | + |
| 61 | + constructor(address _entropy, address _entropyProvider) { |
| 62 | + entropy = IEntropyV2(_entropy); |
| 63 | + entropyProvider = _entropyProvider; |
| 64 | + } |
| 65 | + |
| 66 | + // Submit user data and request credit score calculation |
| 67 | + function submitUserDataAndRequestScore( |
| 68 | + string memory name, |
| 69 | + uint256 value, |
| 70 | + ClosedPosition[] memory closedPositions, |
| 71 | + CurrentPosition[] memory currentPositions |
| 72 | + ) external payable { |
| 73 | + // Clear previous data |
| 74 | + delete userData[msg.sender].closedPositions; |
| 75 | + delete userData[msg.sender].currentPositions; |
| 76 | + |
| 77 | + // Store user data |
| 78 | + userData[msg.sender].name = name; |
| 79 | + userData[msg.sender].value = value; |
| 80 | + |
| 81 | + // Store closed positions |
| 82 | + for (uint i = 0; i < closedPositions.length; i++) { |
| 83 | + userData[msg.sender].closedPositions.push(closedPositions[i]); |
| 84 | + } |
| 85 | + |
| 86 | + // Store current positions |
| 87 | + for (uint i = 0; i < currentPositions.length; i++) { |
| 88 | + userData[msg.sender].currentPositions.push(currentPositions[i]); |
| 89 | + } |
| 90 | + |
| 91 | + // Request entropy for variance |
| 92 | + uint256 fee = entropy.getFeeV2(); |
| 93 | + require(msg.value >= fee, "Insufficient fee for entropy"); |
| 94 | + |
| 95 | + uint64 sequenceNumber = entropy.requestV2{ value: fee }(); |
| 96 | + sequenceToUser[sequenceNumber] = msg.sender; |
| 97 | + userToSequence[msg.sender] = sequenceNumber; |
| 98 | + |
| 99 | + emit CreditScoreRequested(msg.sender, sequenceNumber); |
| 100 | + } |
| 101 | + |
| 102 | + // Calculate base credit score without entropy (for preview purposes) |
| 103 | + function calculateBaseScore(address user) public view returns (uint256) { |
| 104 | + UserData storage data = userData[user]; |
| 105 | + |
| 106 | + if (data.closedPositions.length == 0 && data.currentPositions.length == 0) { |
| 107 | + return 500; // Default middle score for new users |
| 108 | + } |
| 109 | + |
| 110 | + uint256 score = 0; |
| 111 | + uint256 weightSum = 0; |
| 112 | + |
| 113 | + // 1. Calculate profit/loss metrics from closed positions (40% weight) |
| 114 | + if (data.closedPositions.length > 0) { |
| 115 | + int256 totalPnl = 0; |
| 116 | + uint256 totalInvested = 0; |
| 117 | + uint256 winCount = 0; |
| 118 | + |
| 119 | + for (uint i = 0; i < data.closedPositions.length; i++) { |
| 120 | + totalPnl += data.closedPositions[i].realizedPnl; |
| 121 | + totalInvested += data.closedPositions[i].totalBought; |
| 122 | + if (data.closedPositions[i].realizedPnl > 0) { |
| 123 | + winCount++; |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // Win rate (0-200 points) |
| 128 | + uint256 winRate = (winCount * 200) / data.closedPositions.length; |
| 129 | + score += winRate; |
| 130 | + |
| 131 | + // Profit ratio (0-200 points) |
| 132 | + if (totalInvested > 0) { |
| 133 | + if (totalPnl > 0) { |
| 134 | + uint256 profitRatio = (uint256(totalPnl) * 200) / totalInvested; |
| 135 | + if (profitRatio > 200) profitRatio = 200; // Cap at 200 |
| 136 | + score += profitRatio; |
| 137 | + } else { |
| 138 | + // Negative PnL reduces score |
| 139 | + uint256 lossRatio = (uint256(-totalPnl) * 100) / totalInvested; |
| 140 | + if (lossRatio > 200) lossRatio = 200; |
| 141 | + score += 0; // No additional score for losses |
| 142 | + } |
| 143 | + } |
| 144 | + weightSum += 400; |
| 145 | + } |
| 146 | + |
| 147 | + // 2. Current positions performance (30% weight) |
| 148 | + if (data.currentPositions.length > 0) { |
| 149 | + int256 currentTotalPnl = 0; |
| 150 | + uint256 currentTotalInvested = 0; |
| 151 | + |
| 152 | + for (uint i = 0; i < data.currentPositions.length; i++) { |
| 153 | + currentTotalPnl += data.currentPositions[i].cashPnl; |
| 154 | + currentTotalInvested += data.currentPositions[i].totalBought; |
| 155 | + } |
| 156 | + |
| 157 | + // Current position health (0-300 points) |
| 158 | + if (currentTotalInvested > 0) { |
| 159 | + if (currentTotalPnl >= 0) { |
| 160 | + uint256 currentRatio = (uint256(currentTotalPnl) * 150) / currentTotalInvested; |
| 161 | + if (currentRatio > 150) currentRatio = 150; |
| 162 | + score += 150 + currentRatio; // Base 150 + up to 150 bonus |
| 163 | + } else { |
| 164 | + // Losses reduce from base |
| 165 | + uint256 lossRatio = (uint256(-currentTotalPnl) * 150) / currentTotalInvested; |
| 166 | + if (lossRatio > 150) lossRatio = 150; |
| 167 | + score += (150 - lossRatio); |
| 168 | + } |
| 169 | + } else { |
| 170 | + score += 150; // Neutral if no current positions |
| 171 | + } |
| 172 | + weightSum += 300; |
| 173 | + } |
| 174 | + |
| 175 | + // 3. Portfolio value consideration (20% weight) |
| 176 | + if (data.value > 0) { |
| 177 | + // Scale based on value (logarithmic scale) |
| 178 | + uint256 valueScore = 0; |
| 179 | + if (data.value >= 1000000) { |
| 180 | + // 1M+ |
| 181 | + valueScore = 200; |
| 182 | + } else if (data.value >= 500000) { |
| 183 | + // 500k+ |
| 184 | + valueScore = 180; |
| 185 | + } else if (data.value >= 100000) { |
| 186 | + // 100k+ |
| 187 | + valueScore = 150; |
| 188 | + } else if (data.value >= 50000) { |
| 189 | + // 50k+ |
| 190 | + valueScore = 120; |
| 191 | + } else if (data.value >= 10000) { |
| 192 | + // 10k+ |
| 193 | + valueScore = 100; |
| 194 | + } else { |
| 195 | + valueScore = (data.value * 100) / 10000; // Linear scale below 10k |
| 196 | + } |
| 197 | + score += valueScore; |
| 198 | + weightSum += 200; |
| 199 | + } |
| 200 | + |
| 201 | + // 4. Activity bonus (10% weight) |
| 202 | + uint256 totalTrades = data.closedPositions.length + data.currentPositions.length; |
| 203 | + uint256 activityScore = 0; |
| 204 | + if (totalTrades >= 20) { |
| 205 | + activityScore = 100; |
| 206 | + } else if (totalTrades >= 10) { |
| 207 | + activityScore = 80; |
| 208 | + } else if (totalTrades >= 5) { |
| 209 | + activityScore = 60; |
| 210 | + } else if (totalTrades > 0) { |
| 211 | + activityScore = (totalTrades * 60) / 5; |
| 212 | + } |
| 213 | + score += activityScore; |
| 214 | + weightSum += 100; |
| 215 | + |
| 216 | + // Normalize to 0-949 range (leaving room for entropy variance) |
| 217 | + if (weightSum > 0) { |
| 218 | + score = (score * 949) / weightSum; |
| 219 | + } |
| 220 | + |
| 221 | + return score; |
| 222 | + } |
| 223 | + |
| 224 | + // Entropy callback implementation |
| 225 | + function entropyCallback(uint64 sequenceNumber, address, bytes32 randomNumber) internal override { |
| 226 | + address user = sequenceToUser[sequenceNumber]; |
| 227 | + require(user != address(0), "Invalid sequence number"); |
| 228 | + |
| 229 | + // Calculate base score |
| 230 | + uint256 baseScore = calculateBaseScore(user); |
| 231 | + |
| 232 | + // Add entropy-based variance (±ENTROPY_VARIANCE_WEIGHT points) |
| 233 | + uint256 entropyFactor = uint256(randomNumber) % (ENTROPY_VARIANCE_WEIGHT * 2 + 1); |
| 234 | + int256 variance = int256(entropyFactor) - int256(ENTROPY_VARIANCE_WEIGHT); |
| 235 | + |
| 236 | + // Calculate final score with bounds checking |
| 237 | + int256 finalScoreInt = int256(baseScore) + variance; |
| 238 | + uint256 finalScore; |
| 239 | + |
| 240 | + if (finalScoreInt < 0) { |
| 241 | + finalScore = 0; |
| 242 | + } else if (finalScoreInt > int256(MAX_SCORE)) { |
| 243 | + finalScore = MAX_SCORE; |
| 244 | + } else { |
| 245 | + finalScore = uint256(finalScoreInt); |
| 246 | + } |
| 247 | + |
| 248 | + // Store the calculated score |
| 249 | + creditScores[user] = CreditScoreData({ |
| 250 | + score: finalScore, |
| 251 | + timestamp: block.timestamp, |
| 252 | + entropyUsed: randomNumber, |
| 253 | + isCalculated: true |
| 254 | + }); |
| 255 | + |
| 256 | + emit CreditScoreCalculated(user, finalScore, randomNumber); |
| 257 | + |
| 258 | + // Clean up mappings |
| 259 | + delete sequenceToUser[sequenceNumber]; |
| 260 | + delete userToSequence[user]; |
| 261 | + } |
| 262 | + |
| 263 | + // Required by IEntropyConsumer |
| 264 | + function getEntropy() internal view override returns (address) { |
| 265 | + return address(entropy); |
| 266 | + } |
| 267 | + |
| 268 | + // Get the fee required for entropy |
| 269 | + function getEntropyFee() public view returns (uint256) { |
| 270 | + return entropy.getFeeV2(); |
| 271 | + } |
| 272 | + |
| 273 | + // Get user's credit score |
| 274 | + function getCreditScore(address user) external view returns (uint256 score, uint256 timestamp, bool isCalculated) { |
| 275 | + CreditScoreData memory data = creditScores[user]; |
| 276 | + return (data.score, data.timestamp, data.isCalculated); |
| 277 | + } |
| 278 | + |
| 279 | + // Get user's pending sequence number |
| 280 | + function getPendingSequence(address user) external view returns (uint64) { |
| 281 | + return userToSequence[user]; |
| 282 | + } |
| 283 | + |
| 284 | + // Check if user has pending score calculation |
| 285 | + function hasPendingCalculation(address user) external view returns (bool) { |
| 286 | + return userToSequence[user] != 0; |
| 287 | + } |
| 288 | + |
| 289 | + receive() external payable {} |
| 290 | +} |
0 commit comments