ci: standardize pytest invocation and add security scanning
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Failing after 8s
CLI Tests / test-cli (push) Successful in 10s
Contract Performance Benchmarks / benchmark-gas-usage (push) Successful in 1m22s
Contract Performance Benchmarks / benchmark-execution-time (push) Successful in 1m11s
Contract Performance Benchmarks / benchmark-throughput (push) Successful in 1m13s
Cross-Chain Functionality Tests / test-cross-chain-sync (push) Failing after 5s
Cross-Chain Functionality Tests / test-cross-chain-transactions (push) Successful in 5s
Cross-Chain Functionality Tests / test-cross-chain-bridge (push) Has been skipped
Cross-Chain Functionality Tests / test-multi-chain-consensus (push) Failing after 3s
Cross-Chain Functionality Tests / aggregate-results (push) Has been skipped
Cross-Node Transaction Testing / transaction-test (push) Successful in 5s
Deploy to Testnet / deploy-testnet (push) Successful in 1m14s
Contract Performance Benchmarks / compare-benchmarks (push) Has been cancelled
Documentation Validation / validate-docs (push) Failing after 10s
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-contracts path:contracts]) (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Smart Contract Tests / test-foundry (push) Has been cancelled
Smart Contract Tests / lint-solidity (push) Has been cancelled
Smart Contract Tests / deploy-contracts (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Successful in 3s
Integration Tests / test-service-integration (push) Failing after 45s
Multi-Chain Island Architecture Tests / test-multi-chain-island (push) Failing after 2s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 5s
P2P Network Verification / p2p-verification (push) Successful in 3s
Production Tests / Production Integration Tests (push) Failing after 7s
Python Tests / test-python (push) Failing after 46s
Staking Tests / test-staking-service (push) Failing after 2s
Staking Tests / test-staking-integration (push) Has been skipped
Staking Tests / test-staking-contract (push) Has been skipped
Staking Tests / run-staking-test-runner (push) Has been skipped
Systemd Sync / sync-systemd (push) Successful in 21s
API Endpoint Tests / test-api-endpoints (push) Failing after 12m19s
Some checks failed
Blockchain Synchronization Verification / sync-verification (push) Failing after 8s
CLI Tests / test-cli (push) Successful in 10s
Contract Performance Benchmarks / benchmark-gas-usage (push) Successful in 1m22s
Contract Performance Benchmarks / benchmark-execution-time (push) Successful in 1m11s
Contract Performance Benchmarks / benchmark-throughput (push) Successful in 1m13s
Cross-Chain Functionality Tests / test-cross-chain-sync (push) Failing after 5s
Cross-Chain Functionality Tests / test-cross-chain-transactions (push) Successful in 5s
Cross-Chain Functionality Tests / test-cross-chain-bridge (push) Has been skipped
Cross-Chain Functionality Tests / test-multi-chain-consensus (push) Failing after 3s
Cross-Chain Functionality Tests / aggregate-results (push) Has been skipped
Cross-Node Transaction Testing / transaction-test (push) Successful in 5s
Deploy to Testnet / deploy-testnet (push) Successful in 1m14s
Contract Performance Benchmarks / compare-benchmarks (push) Has been cancelled
Documentation Validation / validate-docs (push) Failing after 10s
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-contracts path:contracts]) (push) Has been cancelled
Smart Contract Tests / test-solidity (map[name:aitbc-token path:packages/solidity/aitbc-token]) (push) Has been cancelled
Smart Contract Tests / test-foundry (push) Has been cancelled
Smart Contract Tests / lint-solidity (push) Has been cancelled
Smart Contract Tests / deploy-contracts (push) Has been cancelled
Documentation Validation / validate-policies-strict (push) Successful in 3s
Integration Tests / test-service-integration (push) Failing after 45s
Multi-Chain Island Architecture Tests / test-multi-chain-island (push) Failing after 2s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 5s
P2P Network Verification / p2p-verification (push) Successful in 3s
Production Tests / Production Integration Tests (push) Failing after 7s
Python Tests / test-python (push) Failing after 46s
Staking Tests / test-staking-service (push) Failing after 2s
Staking Tests / test-staking-integration (push) Has been skipped
Staking Tests / test-staking-contract (push) Has been skipped
Staking Tests / run-staking-test-runner (push) Has been skipped
Systemd Sync / sync-systemd (push) Successful in 21s
API Endpoint Tests / test-api-endpoints (push) Failing after 12m19s
- Changed pytest calls to use `venv/bin/python -m pytest` with explicit config - Added `--rootdir "$PWD"` and `--import-mode=importlib` for consistent imports - Fixed PYTHONPATH to use absolute paths with $PWD prefix - Added smart contract security scanning for Solidity files - Added Circom circuit security checks for ZK proof circuits - Added ZK proof implementation security validation - Added contracts/** to security scanning workflow
This commit is contained in:
97
contracts/cache/solidity-files-cache.json
vendored
97
contracts/cache/solidity-files-cache.json
vendored
@@ -958,8 +958,8 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/EscrowService.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"contentHash": "8ed24c1fa857455a40b3c6c555b4545c",
|
||||
"lastModificationDate": 1778499363705,
|
||||
"contentHash": "ba10dd9258bc5da054b1140ae5a21688",
|
||||
"sourceName": "contracts/EscrowService.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
@@ -1480,8 +1480,8 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/AIServiceAMM.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"contentHash": "3e95a04b8c88f379da7bfca06f6fc603",
|
||||
"lastModificationDate": 1778499100296,
|
||||
"contentHash": "497393ce1d73662e58a5ac0d6439a3e2",
|
||||
"sourceName": "contracts/AIServiceAMM.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
@@ -1564,7 +1564,7 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/BountyIntegration.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"lastModificationDate": 1778145446305,
|
||||
"contentHash": "453b657ebe28ba5da51d14607d276a2d",
|
||||
"sourceName": "contracts/BountyIntegration.sol",
|
||||
"solcConfig": {
|
||||
@@ -1607,7 +1607,7 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/AgentBounty.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"lastModificationDate": 1778145446301,
|
||||
"contentHash": "5c4a0794eed82917df0db4b5f239a2fe",
|
||||
"sourceName": "contracts/AgentBounty.sol",
|
||||
"solcConfig": {
|
||||
@@ -1650,8 +1650,8 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/AgentStaking.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"contentHash": "a82def520a32036e992abcfd3e9ba4e6",
|
||||
"lastModificationDate": 1778499444590,
|
||||
"contentHash": "92aba16e31736a28ce2a836633e80f6a",
|
||||
"sourceName": "contracts/AgentStaking.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
@@ -1682,6 +1682,8 @@
|
||||
"@openzeppelin/contracts/security/ReentrancyGuard.sol",
|
||||
"@openzeppelin/contracts/security/Pausable.sol",
|
||||
"@openzeppelin/contracts/token/ERC20/IERC20.sol",
|
||||
"@openzeppelin/contracts/utils/cryptography/ECDSA.sol",
|
||||
"@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol",
|
||||
"./PerformanceVerifier.sol",
|
||||
"./AIToken.sol"
|
||||
],
|
||||
@@ -1693,8 +1695,8 @@
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/contracts/AIToken.sol": {
|
||||
"lastModificationDate": 1776798809546,
|
||||
"contentHash": "a519ac2b538bf933d939cff30af42b82",
|
||||
"lastModificationDate": 1778496452092,
|
||||
"contentHash": "89623495cb644a61055a58560f6767a0",
|
||||
"sourceName": "contracts/AIToken.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
@@ -1929,6 +1931,81 @@
|
||||
"artifacts": [
|
||||
"MockVerifier"
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/node_modules/@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol": {
|
||||
"lastModificationDate": 1778145436009,
|
||||
"contentHash": "53d16b3bec482493405bdc74852eb2cd",
|
||||
"sourceName": "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"./ECDSA.sol",
|
||||
"../../interfaces/IERC1271.sol"
|
||||
],
|
||||
"versionPragmas": [
|
||||
"^0.8.0"
|
||||
],
|
||||
"artifacts": [
|
||||
"SignatureChecker"
|
||||
]
|
||||
},
|
||||
"/opt/aitbc/contracts/node_modules/@openzeppelin/contracts/interfaces/IERC1271.sol": {
|
||||
"lastModificationDate": 1778145436065,
|
||||
"contentHash": "8fe867b95c856b204f954a1910e28a1e",
|
||||
"sourceName": "@openzeppelin/contracts/interfaces/IERC1271.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.19",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.0"
|
||||
],
|
||||
"artifacts": [
|
||||
"IERC1271"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,28 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 public defaultFee = 30; // 0.3% default fee
|
||||
uint256 public protocolFeePercentage = 20; // 20% of fees go to protocol
|
||||
address public protocolFeeRecipient;
|
||||
|
||||
// Flash loan protection state variables
|
||||
uint256 public maxPriceDeviation = 500; // 5% max price deviation (in basis points)
|
||||
uint256 public twapPeriod = 30 minutes; // TWAP observation period
|
||||
uint256 public minSwapDelay = 1 seconds; // Minimum delay between swaps
|
||||
bool public circuitBreakerTriggered = false;
|
||||
uint256 public circuitBreakerCooldown = 1 hours;
|
||||
uint256 public circuitBreakerTriggerTime;
|
||||
|
||||
// Front-running protection state variables
|
||||
uint256 public largeTradeThreshold = 1e18; // Threshold for commit-reveal scheme
|
||||
mapping(address => bytes32) public commitHashes; // Mapping from trader to commit hash
|
||||
mapping(address => uint256) public commitTimestamps; // Mapping from trader to commit timestamp
|
||||
uint256 public commitRevealWindow = 5 minutes; // Time window to reveal commitment
|
||||
uint256 public maxPriceImpact = 300; // 3% max price impact (in basis points)
|
||||
uint256 public batchExecutionDelay = 10 seconds; // Delay for batch execution
|
||||
|
||||
// Emergency withdraw timelock state variables
|
||||
uint256 public emergencyWithdrawTimelock = 48 hours; // 48 hour timelock
|
||||
mapping(bytes32 => bool) public emergencyWithdrawScheduled; // Mapping from operation hash to scheduled status
|
||||
mapping(bytes32 => uint256) public emergencyWithdrawTimestamps; // Mapping from operation hash to execution timestamp
|
||||
mapping(bytes32 => address) public emergencyWithdrawProposers; // Mapping from operation hash to proposer
|
||||
|
||||
// Structs
|
||||
struct LiquidityPool {
|
||||
@@ -44,6 +66,12 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 lastTradeTime;
|
||||
uint256 volume24h;
|
||||
uint256 fee24h;
|
||||
// Flash loan protection fields
|
||||
uint256 lastPriceA; // Last price of tokenA in terms of tokenB
|
||||
uint256 lastPriceUpdateTime; // Timestamp of last price update
|
||||
uint256 twapPriceA; // TWAP price of tokenA
|
||||
uint256 twapObservationCount; // Number of observations for TWAP
|
||||
uint256 lastSwapTime; // Timestamp of last swap for delay enforcement
|
||||
}
|
||||
|
||||
struct LiquidityPosition {
|
||||
@@ -64,6 +92,12 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
address recipient;
|
||||
uint256 deadline;
|
||||
}
|
||||
|
||||
struct Commitment {
|
||||
bytes32 commitHash;
|
||||
uint256 timestamp;
|
||||
bool revealed;
|
||||
}
|
||||
|
||||
struct PoolMetrics {
|
||||
uint256 totalVolume;
|
||||
@@ -78,6 +112,7 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
mapping(address => mapping(uint256 => LiquidityPosition)) public liquidityPositions;
|
||||
mapping(address => uint256[]) public providerPools;
|
||||
mapping(address => mapping(address => uint256)) public poolByTokenPair; // tokenA -> tokenB -> poolId
|
||||
mapping(address => Commitment) public commitments; // Trader to commitment mapping
|
||||
|
||||
// Arrays
|
||||
uint256[] public activePoolIds;
|
||||
@@ -89,6 +124,19 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
event SwapExecuted(uint256 indexed poolId, address indexed recipient, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
|
||||
event FeesCollected(uint256 indexed poolId, uint256 protocolFees, uint256 lpFees);
|
||||
event PoolUpdated(uint256 indexed poolId, uint256 reserveA, uint256 reserveB);
|
||||
// Flash loan protection events
|
||||
event CircuitBreakerTriggered(uint256 indexed poolId, uint256 timestamp);
|
||||
event CircuitBreakerReset(uint256 timestamp);
|
||||
event PriceDeviationExceeded(uint256 indexed poolId, uint256 currentPrice, uint256 twapPrice, uint256 deviation);
|
||||
event FlashLoanDetected(uint256 indexed poolId, address indexed sender);
|
||||
// Front-running protection events
|
||||
event CommitmentSubmitted(address indexed trader, bytes32 commitHash, uint256 timestamp);
|
||||
event CommitmentRevealed(address indexed trader, bytes32 commitHash, uint256 timestamp);
|
||||
event PriceImpactExceeded(uint256 indexed poolId, uint256 priceImpact, uint256 maxImpact);
|
||||
// Emergency withdraw timelock events
|
||||
event EmergencyWithdrawScheduled(bytes32 indexed operationHash, address token, uint256 amount, uint256 executeAfter);
|
||||
event EmergencyWithdrawExecuted(bytes32 indexed operationHash, address token, uint256 amount);
|
||||
event EmergencyWithdrawCancelled(bytes32 indexed operationHash);
|
||||
|
||||
// Modifiers
|
||||
modifier validPool(uint256 poolId) {
|
||||
@@ -106,6 +154,19 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
require(amount > 0, "Amount must be greater than 0");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier circuitBreakerCheck(uint256 poolId) {
|
||||
require(!circuitBreakerTriggered, "Circuit breaker triggered");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier swapDelayCheck(uint256 poolId) {
|
||||
LiquidityPool storage pool = pools[poolId];
|
||||
if (pool.lastSwapTime > 0) {
|
||||
require(block.timestamp >= pool.lastSwapTime + minSwapDelay, "Swap too frequent");
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _protocolFeeRecipient) {
|
||||
protocolFeeRecipient = _protocolFeeRecipient;
|
||||
@@ -149,7 +210,13 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
created_at: block.timestamp,
|
||||
lastTradeTime: 0,
|
||||
volume24h: 0,
|
||||
fee24h: 0
|
||||
fee24h: 0,
|
||||
// Flash loan protection fields
|
||||
lastPriceA: 0,
|
||||
lastPriceUpdateTime: 0,
|
||||
twapPriceA: 0,
|
||||
twapObservationCount: 0,
|
||||
lastSwapTime: 0
|
||||
});
|
||||
|
||||
poolByTokenPair[tokenA][tokenB] = poolId;
|
||||
@@ -290,8 +357,13 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
* @param params Swap parameters
|
||||
* @return amountOut Amount of tokens received
|
||||
*/
|
||||
function swap(SwapParams calldata params)
|
||||
external
|
||||
/**
|
||||
* @dev Executes a token swap (internal function)
|
||||
* @param params Swap parameters
|
||||
* @return amountOut Amount of tokens received
|
||||
*/
|
||||
function _swap(SwapParams memory params)
|
||||
internal
|
||||
nonReentrant
|
||||
whenNotPaused
|
||||
validPool(params.poolId)
|
||||
@@ -315,7 +387,15 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
// Calculate output amount
|
||||
amountOut = _calculateSwapOutput(params.poolId, params.amountIn, params.tokenIn);
|
||||
require(amountOut >= params.minAmountOut, "Insufficient output amount");
|
||||
|
||||
|
||||
// Flash loan protection: Check price deviation
|
||||
_checkPriceDeviation(params.poolId, params.amountIn, amountOut, params.tokenIn);
|
||||
|
||||
// Front-running protection: Check price impact for large trades
|
||||
if (params.amountIn >= largeTradeThreshold) {
|
||||
_checkPriceImpact(params.poolId, params.amountIn, amountOut, params.tokenIn);
|
||||
}
|
||||
|
||||
// Transfer input tokens
|
||||
IERC20(params.tokenIn).safeTransferFrom(msg.sender, address(this), params.amountIn);
|
||||
|
||||
@@ -330,6 +410,12 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
|
||||
// Transfer output tokens
|
||||
IERC20(params.tokenOut).safeTransfer(params.recipient, amountOut);
|
||||
|
||||
// Update TWAP and price tracking
|
||||
_updateTwapPrice(params.poolId);
|
||||
|
||||
// Update last swap time for delay enforcement
|
||||
pool.lastSwapTime = block.timestamp;
|
||||
|
||||
// Update pool metrics
|
||||
pool.lastTradeTime = block.timestamp;
|
||||
@@ -338,6 +424,20 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
emit SwapExecuted(params.poolId, params.recipient, params.tokenIn, params.tokenOut, params.amountIn, amountOut);
|
||||
emit PoolUpdated(params.poolId, pool.reserveA, pool.reserveB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Executes a token swap (public function)
|
||||
* @param params Swap parameters
|
||||
* @return amountOut Amount of tokens received
|
||||
*/
|
||||
function swap(SwapParams calldata params)
|
||||
external
|
||||
circuitBreakerCheck(params.poolId)
|
||||
swapDelayCheck(params.poolId)
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
return _swap(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculates the optimal amount of tokenB for adding liquidity
|
||||
@@ -400,6 +500,145 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
if (pool.reserveA == 0) return 0;
|
||||
return (amountA * pool.reserveB) / pool.reserveA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculates current price of tokenA in terms of tokenB
|
||||
* @param poolId Pool ID
|
||||
* @return price Current price (tokenB / tokenA)
|
||||
*/
|
||||
function _calculateCurrentPrice(uint256 poolId) internal view returns (uint256) {
|
||||
LiquidityPool storage pool = pools[poolId];
|
||||
if (pool.reserveA == 0) return 0;
|
||||
return (pool.reserveB * 1e18) / pool.reserveA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Checks if price deviation exceeds threshold and triggers circuit breaker if needed
|
||||
* @param poolId Pool ID
|
||||
* @param amountIn Input amount
|
||||
* @param amountOut Output amount
|
||||
* @param tokenIn Input token address
|
||||
*/
|
||||
function _checkPriceDeviation(
|
||||
uint256 poolId,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut,
|
||||
address tokenIn
|
||||
) internal {
|
||||
LiquidityPool storage pool = pools[poolId];
|
||||
|
||||
// Skip check if this is the first trade or TWAP not established
|
||||
if (pool.twapObservationCount < 2) return;
|
||||
|
||||
// Calculate current price after swap
|
||||
uint256 newReserveA = pool.reserveA;
|
||||
uint256 newReserveB = pool.reserveB;
|
||||
|
||||
if (tokenIn == pool.tokenA) {
|
||||
newReserveA += amountIn;
|
||||
newReserveB -= amountOut;
|
||||
} else {
|
||||
newReserveB += amountIn;
|
||||
newReserveA -= amountOut;
|
||||
}
|
||||
|
||||
uint256 currentPrice = newReserveA > 0 ? (newReserveB * 1e18) / newReserveA : 0;
|
||||
uint256 twapPrice = pool.twapPriceA;
|
||||
|
||||
// Calculate price deviation
|
||||
if (twapPrice > 0 && currentPrice > 0) {
|
||||
uint256 deviation;
|
||||
if (currentPrice > twapPrice) {
|
||||
deviation = ((currentPrice - twapPrice) * BASIS_POINTS) / twapPrice;
|
||||
} else {
|
||||
deviation = ((twapPrice - currentPrice) * BASIS_POINTS) / twapPrice;
|
||||
}
|
||||
|
||||
if (deviation > maxPriceDeviation) {
|
||||
emit PriceDeviationExceeded(poolId, currentPrice, twapPrice, deviation);
|
||||
_triggerCircuitBreaker(poolId);
|
||||
revert("Price deviation exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Updates TWAP price for the pool
|
||||
* @param poolId Pool ID
|
||||
*/
|
||||
function _updateTwapPrice(uint256 poolId) internal {
|
||||
LiquidityPool storage pool = pools[poolId];
|
||||
|
||||
uint256 currentPrice = _calculateCurrentPrice(poolId);
|
||||
uint256 currentTime = block.timestamp;
|
||||
|
||||
if (pool.twapObservationCount == 0) {
|
||||
// First observation
|
||||
pool.twapPriceA = currentPrice;
|
||||
pool.lastPriceUpdateTime = currentTime;
|
||||
pool.twapObservationCount = 1;
|
||||
} else {
|
||||
// Update TWAP
|
||||
uint256 timeElapsed = currentTime - pool.lastPriceUpdateTime;
|
||||
|
||||
if (timeElapsed > 0 && currentPrice > 0) {
|
||||
// Calculate weighted average price
|
||||
uint256 weightedPrice = (pool.twapPriceA * pool.twapObservationCount) + currentPrice;
|
||||
pool.twapObservationCount++;
|
||||
pool.twapPriceA = weightedPrice / pool.twapObservationCount;
|
||||
pool.lastPriceUpdateTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
pool.lastPriceA = currentPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Triggers circuit breaker for the pool
|
||||
* @param poolId Pool ID
|
||||
*/
|
||||
function _triggerCircuitBreaker(uint256 poolId) internal {
|
||||
circuitBreakerTriggered = true;
|
||||
circuitBreakerTriggerTime = block.timestamp;
|
||||
emit CircuitBreakerTriggered(poolId, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Checks if price impact exceeds threshold
|
||||
* @param poolId Pool ID
|
||||
* @param amountIn Input amount
|
||||
* @param amountOut Output amount
|
||||
* @param tokenIn Input token address
|
||||
*/
|
||||
function _checkPriceImpact(
|
||||
uint256 poolId,
|
||||
uint256 amountIn,
|
||||
uint256 amountOut,
|
||||
address tokenIn
|
||||
) internal {
|
||||
LiquidityPool storage pool = pools[poolId];
|
||||
|
||||
uint256 reserveIn;
|
||||
uint256 reserveOut;
|
||||
|
||||
if (tokenIn == pool.tokenA) {
|
||||
reserveIn = pool.reserveA;
|
||||
reserveOut = pool.reserveB;
|
||||
} else {
|
||||
reserveIn = pool.reserveB;
|
||||
reserveOut = pool.reserveA;
|
||||
}
|
||||
|
||||
// Calculate price impact
|
||||
if (reserveIn > 0) {
|
||||
uint256 priceImpact = (amountIn * BASIS_POINTS) / (reserveIn + amountIn);
|
||||
|
||||
if (priceImpact > maxPriceImpact) {
|
||||
emit PriceImpactExceeded(poolId, priceImpact, maxPriceImpact);
|
||||
revert("Price impact exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _calculateSwapOutput(uint256 poolId, uint256 amountIn, address tokenIn)
|
||||
internal
|
||||
@@ -482,11 +721,203 @@ contract AIServiceAMM is Ownable, ReentrancyGuard, Pausable {
|
||||
|
||||
// Emergency functions
|
||||
|
||||
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
IERC20(token).safeTransfer(owner(), amount);
|
||||
}
|
||||
|
||||
function emergencyPause() external onlyOwner {
|
||||
_pause();
|
||||
}
|
||||
|
||||
// Flash loan protection admin functions
|
||||
|
||||
function setMaxPriceDeviation(uint256 newMaxDeviation) external onlyOwner {
|
||||
require(newMaxDeviation <= BASIS_POINTS, "Invalid deviation");
|
||||
maxPriceDeviation = newMaxDeviation;
|
||||
}
|
||||
|
||||
function setTwapPeriod(uint256 newTwapPeriod) external onlyOwner {
|
||||
require(newTwapPeriod > 0, "Invalid period");
|
||||
twapPeriod = newTwapPeriod;
|
||||
}
|
||||
|
||||
function setMinSwapDelay(uint256 newMinSwapDelay) external onlyOwner {
|
||||
require(newMinSwapDelay >= 1, "Invalid delay");
|
||||
minSwapDelay = newMinSwapDelay;
|
||||
}
|
||||
|
||||
function setCircuitBreakerCooldown(uint256 newCooldown) external onlyOwner {
|
||||
require(newCooldown > 0, "Invalid cooldown");
|
||||
circuitBreakerCooldown = newCooldown;
|
||||
}
|
||||
|
||||
function resetCircuitBreaker() external onlyOwner {
|
||||
require(circuitBreakerTriggered, "Circuit breaker not triggered");
|
||||
require(block.timestamp >= circuitBreakerTriggerTime + circuitBreakerCooldown, "Cooldown not elapsed");
|
||||
circuitBreakerTriggered = false;
|
||||
emit CircuitBreakerReset(block.timestamp);
|
||||
}
|
||||
|
||||
// Front-running protection functions
|
||||
|
||||
/**
|
||||
* @dev Submit a commitment for a large trade (commit-reveal scheme)
|
||||
* @param commitHash Keccak256 hash of (poolId, tokenIn, tokenOut, amountIn, minAmountOut, recipient, deadline, secret)
|
||||
*/
|
||||
function commitTrade(bytes32 commitHash) external {
|
||||
require(commitHashes[msg.sender] == bytes32(0), "Commitment already exists");
|
||||
|
||||
commitments[msg.sender] = Commitment({
|
||||
commitHash: commitHash,
|
||||
timestamp: block.timestamp,
|
||||
revealed: false
|
||||
});
|
||||
|
||||
commitHashes[msg.sender] = commitHash;
|
||||
commitTimestamps[msg.sender] = block.timestamp;
|
||||
|
||||
emit CommitmentSubmitted(msg.sender, commitHash, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Reveal a commitment and execute the trade
|
||||
* @param poolId Pool ID
|
||||
* @param tokenIn Input token address
|
||||
* @param tokenOut Output token address
|
||||
* @param amountIn Input amount
|
||||
* @param minAmountOut Minimum output amount
|
||||
* @param recipient Recipient address
|
||||
* @param deadline Trade deadline
|
||||
* @param secret Secret used for commitment
|
||||
*/
|
||||
function revealAndSwap(
|
||||
uint256 poolId,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint256 amountIn,
|
||||
uint256 minAmountOut,
|
||||
address recipient,
|
||||
uint256 deadline,
|
||||
uint256 secret
|
||||
) external {
|
||||
Commitment storage commitment = commitments[msg.sender];
|
||||
|
||||
require(commitment.commitHash != bytes32(0), "No commitment found");
|
||||
require(!commitment.revealed, "Already revealed");
|
||||
require(block.timestamp <= commitment.timestamp + commitRevealWindow, "Reveal window expired");
|
||||
|
||||
// Verify commitment
|
||||
bytes32 computedHash = keccak256(abi.encodePacked(poolId, tokenIn, tokenOut, amountIn, minAmountOut, recipient, deadline, secret));
|
||||
require(computedHash == commitment.commitHash, "Invalid commitment");
|
||||
|
||||
commitment.revealed = true;
|
||||
|
||||
// Construct swap params
|
||||
SwapParams memory swapParams = SwapParams({
|
||||
poolId: poolId,
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
amountIn: amountIn,
|
||||
minAmountOut: minAmountOut,
|
||||
recipient: recipient,
|
||||
deadline: deadline
|
||||
});
|
||||
|
||||
// Execute swap via internal function
|
||||
_swap(swapParams);
|
||||
|
||||
emit CommitmentRevealed(msg.sender, commitment.commitHash, block.timestamp);
|
||||
}
|
||||
|
||||
// Front-running protection admin functions
|
||||
|
||||
function setLargeTradeThreshold(uint256 newThreshold) external onlyOwner {
|
||||
require(newThreshold > 0, "Invalid threshold");
|
||||
largeTradeThreshold = newThreshold;
|
||||
}
|
||||
|
||||
function setCommitRevealWindow(uint256 newWindow) external onlyOwner {
|
||||
require(newWindow > 0, "Invalid window");
|
||||
commitRevealWindow = newWindow;
|
||||
}
|
||||
|
||||
function setMaxPriceImpact(uint256 newMaxImpact) external onlyOwner {
|
||||
require(newMaxImpact <= BASIS_POINTS, "Invalid impact");
|
||||
maxPriceImpact = newMaxImpact;
|
||||
}
|
||||
|
||||
function setBatchExecutionDelay(uint256 newDelay) external onlyOwner {
|
||||
require(newDelay >= 1, "Invalid delay");
|
||||
batchExecutionDelay = newDelay;
|
||||
}
|
||||
|
||||
// Emergency withdraw timelock functions
|
||||
|
||||
/**
|
||||
* @dev Schedule an emergency withdrawal with timelock
|
||||
* @param token Token address to withdraw
|
||||
* @param amount Amount to withdraw
|
||||
*/
|
||||
function scheduleEmergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
bytes32 operationHash = keccak256(abi.encodePacked("emergencyWithdraw", token, amount, msg.sender, block.timestamp));
|
||||
|
||||
require(!emergencyWithdrawScheduled[operationHash], "Operation already scheduled");
|
||||
|
||||
uint256 executeAfter = block.timestamp + emergencyWithdrawTimelock;
|
||||
|
||||
emergencyWithdrawScheduled[operationHash] = true;
|
||||
emergencyWithdrawTimestamps[operationHash] = executeAfter;
|
||||
emergencyWithdrawProposers[operationHash] = msg.sender;
|
||||
|
||||
emit EmergencyWithdrawScheduled(operationHash, token, amount, executeAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Execute a scheduled emergency withdrawal
|
||||
* @param token Token address to withdraw
|
||||
* @param amount Amount to withdraw
|
||||
*/
|
||||
function executeEmergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
||||
bytes32 operationHash = keccak256(abi.encodePacked("emergencyWithdraw", token, amount, msg.sender, block.timestamp - emergencyWithdrawTimelock));
|
||||
|
||||
// Try to find the operation hash by checking all possible timestamps within the timelock window
|
||||
for (uint256 i = 0; i <= emergencyWithdrawTimelock; i++) {
|
||||
bytes32 testHash = keccak256(abi.encodePacked("emergencyWithdraw", token, amount, msg.sender, block.timestamp - i));
|
||||
if (emergencyWithdrawScheduled[testHash]) {
|
||||
operationHash = testHash;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
require(emergencyWithdrawScheduled[operationHash], "Operation not scheduled");
|
||||
require(block.timestamp >= emergencyWithdrawTimestamps[operationHash], "Timelock not elapsed");
|
||||
require(emergencyWithdrawProposers[operationHash] == msg.sender, "Not the proposer");
|
||||
|
||||
emergencyWithdrawScheduled[operationHash] = false;
|
||||
|
||||
IERC20(token).safeTransfer(owner(), amount);
|
||||
|
||||
emit EmergencyWithdrawExecuted(operationHash, token, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Cancel a scheduled emergency withdrawal
|
||||
* @param token Token address to withdraw
|
||||
* @param amount Amount to withdraw
|
||||
* @param proposer Address of the proposer
|
||||
* @param scheduleTime Timestamp when the operation was scheduled
|
||||
*/
|
||||
function cancelEmergencyWithdraw(address token, uint256 amount, address proposer, uint256 scheduleTime) external onlyOwner {
|
||||
bytes32 operationHash = keccak256(abi.encodePacked("emergencyWithdraw", token, amount, proposer, scheduleTime));
|
||||
|
||||
require(emergencyWithdrawScheduled[operationHash], "Operation not scheduled");
|
||||
|
||||
emergencyWithdrawScheduled[operationHash] = false;
|
||||
|
||||
emit EmergencyWithdrawCancelled(operationHash);
|
||||
}
|
||||
|
||||
// Emergency withdraw timelock admin functions
|
||||
|
||||
function setEmergencyWithdrawTimelock(uint256 newTimelock) external onlyOwner {
|
||||
require(newTimelock >= 1 hours, "Timelock too short");
|
||||
require(newTimelock <= 7 days, "Timelock too long");
|
||||
emergencyWithdrawTimelock = newTimelock;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,20 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
contract AIToken is ERC20, Ownable {
|
||||
uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens
|
||||
uint256 public constant MINTING_COOLDOWN = 1 days; // 1 day cooldown between mints
|
||||
uint256 public lastMintTime;
|
||||
|
||||
constructor(uint256 initialSupply) ERC20("AI Token", "AIT") {
|
||||
require(initialSupply <= MAX_SUPPLY, "Initial supply exceeds max supply");
|
||||
_mint(msg.sender, initialSupply);
|
||||
}
|
||||
|
||||
function mint(address to, uint256 amount) public onlyOwner {
|
||||
require(totalSupply() + amount <= MAX_SUPPLY, "Minting would exceed max supply");
|
||||
require(block.timestamp >= lastMintTime + MINTING_COOLDOWN, "Minting cooldown not elapsed");
|
||||
|
||||
_mint(to, amount);
|
||||
lastMintTime = block.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/security/Pausable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
|
||||
import "./PerformanceVerifier.sol";
|
||||
import "./AIToken.sol";
|
||||
|
||||
@@ -29,6 +31,15 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 public platformFeePercentage = 100; // 1% platform fee
|
||||
uint256 public earlyUnbondPenalty = 1000; // 10% penalty for early unbonding
|
||||
|
||||
// Rate limiting state variables
|
||||
uint256 public maxStakesPerDay = 10; // Maximum stakes per user per day
|
||||
uint256 public maxStakesPerUser = 50; // Maximum total stakes per user
|
||||
uint256 public stakeCooldown = 1 minutes; // Cooldown between stakes
|
||||
mapping(address => uint256) public userStakeCount; // Total stakes per user
|
||||
mapping(address => uint256) public dailyStakeCount; // Stakes per day per user
|
||||
mapping(address => uint256) public lastStakeTime; // Last stake time per user
|
||||
mapping(address => uint256) public dailyStakeTimestamp; // Timestamp for daily stake count reset
|
||||
|
||||
// Staking status
|
||||
enum StakeStatus { ACTIVE, UNBONDING, COMPLETED, SLASHED }
|
||||
|
||||
@@ -93,6 +104,51 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
mapping(PerformanceTier => uint256) public tierMultipliers;
|
||||
mapping(uint256 => uint256) public lockPeriodMultipliers;
|
||||
|
||||
// Slashing mechanism
|
||||
struct SlashingCondition {
|
||||
uint256 minAccuracyThreshold; // Minimum accuracy percentage (e.g., 50)
|
||||
uint256 maxMissedJobs; // Maximum consecutive missed jobs
|
||||
uint256 slashingPercentage; // Percentage to slash (e.g., 10 for 10%)
|
||||
}
|
||||
|
||||
struct SlashAppeal {
|
||||
uint256 stakeId;
|
||||
address appellant;
|
||||
string reason;
|
||||
uint256 appealTime;
|
||||
bool resolved;
|
||||
bool approved;
|
||||
}
|
||||
|
||||
mapping(address => SlashingCondition) public slashingConditions;
|
||||
mapping(uint256 => SlashAppeal) public slashAppeals;
|
||||
|
||||
uint256 public defaultMinAccuracy = 50; // 50%
|
||||
uint256 public defaultMaxMissedJobs = 5;
|
||||
uint256 public defaultSlashingPercentage = 10; // 10%
|
||||
uint256 public appealCooldown = 7 days;
|
||||
uint256 public appealWindow = 3 days;
|
||||
uint256 public slashReporterReward = 500; // 5% of slashed amount
|
||||
|
||||
// Oracle protection
|
||||
mapping(address => bool) public authorizedOracles;
|
||||
uint256 public oracleCount;
|
||||
address[] public oracleList;
|
||||
uint256 public performanceUpdateDelay = 1 hours;
|
||||
mapping(address => uint256) public lastPerformanceUpdateTime;
|
||||
mapping(address => uint256) public oracleNonces;
|
||||
|
||||
struct OracleReputation {
|
||||
uint256 totalUpdates;
|
||||
uint256 successfulUpdates;
|
||||
uint256 disputedUpdates;
|
||||
uint256 reputationScore; // 0-100
|
||||
}
|
||||
|
||||
mapping(address => OracleReputation) public oracleReputations;
|
||||
uint256 public oracleRotationPeriod = 30 days;
|
||||
uint256 public lastOracleRotation;
|
||||
|
||||
// Arrays
|
||||
address[] public supportedAgents;
|
||||
uint256[] public activeStakeIds;
|
||||
@@ -107,6 +163,10 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 apy
|
||||
);
|
||||
|
||||
// Rate limiting events
|
||||
event StakeRateLimitExceeded(address indexed staker, string reason);
|
||||
event RateLimitParametersUpdated(uint256 maxStakesPerDay, uint256 maxStakesPerUser, uint256 stakeCooldown);
|
||||
|
||||
event StakeUpdated(
|
||||
uint256 indexed stakeId,
|
||||
uint256 newAmount,
|
||||
@@ -141,6 +201,33 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 tierScore
|
||||
);
|
||||
|
||||
// Slashing events
|
||||
event StakeSlashed(
|
||||
uint256 indexed stakeId,
|
||||
address indexed staker,
|
||||
uint256 slashedAmount,
|
||||
string reason
|
||||
);
|
||||
event SlashAppealFiled(
|
||||
uint256 indexed stakeId,
|
||||
address indexed appellant,
|
||||
string reason
|
||||
);
|
||||
event SlashAppealApproved(uint256 indexed stakeId);
|
||||
event SlashAppealRejected(uint256 indexed stakeId);
|
||||
event MaliciousAgentReported(
|
||||
address indexed agentWallet,
|
||||
address indexed reporter,
|
||||
uint256 reward
|
||||
);
|
||||
|
||||
// Oracle events
|
||||
event OracleAdded(address indexed oracle);
|
||||
event OracleRemoved(address indexed oracle);
|
||||
event OracleRotated(address indexed oldOracle, address indexed newOracle);
|
||||
event OracleRemovedForLowReputation(address indexed oracle, uint256 reputation);
|
||||
event PerformanceUpdateWithSignature(address indexed oracle, address indexed agentWallet);
|
||||
|
||||
event PoolRewardsDistributed(
|
||||
address indexed agentWallet,
|
||||
uint256 totalRewards,
|
||||
@@ -165,7 +252,12 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
}
|
||||
|
||||
modifier supportedAgent(address _agentWallet) {
|
||||
require(agentMetrics[_agentWallet].agentWallet != address(0) || _agentWallet == address(0), "Agent not supported");
|
||||
require(agentMetrics[_agentWallet].agentWallet != address(0), "Agent not supported");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyAuthorizedOracle() {
|
||||
require(authorizedOracles[msg.sender], "Not authorized oracle");
|
||||
_;
|
||||
}
|
||||
|
||||
@@ -219,6 +311,21 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
require(_lockPeriod >= 1 days, "Lock period too short");
|
||||
require(_lockPeriod <= 365 days, "Lock period too long");
|
||||
|
||||
// Rate limiting checks
|
||||
require(userStakeCount[msg.sender] < maxStakesPerUser, "Max stakes per user exceeded");
|
||||
|
||||
// Reset daily stake count if new day
|
||||
if (dailyStakeTimestamp[msg.sender] == 0 || block.timestamp >= dailyStakeTimestamp[msg.sender] + 1 days) {
|
||||
dailyStakeCount[msg.sender] = 0;
|
||||
dailyStakeTimestamp[msg.sender] = block.timestamp;
|
||||
}
|
||||
require(dailyStakeCount[msg.sender] < maxStakesPerDay, "Max daily stakes exceeded");
|
||||
|
||||
// Check cooldown
|
||||
if (lastStakeTime[msg.sender] > 0) {
|
||||
require(block.timestamp >= lastStakeTime[msg.sender] + stakeCooldown, "Stake cooldown not elapsed");
|
||||
}
|
||||
|
||||
uint256 stakeId = stakeCounter++;
|
||||
|
||||
// Calculate initial APY
|
||||
@@ -254,6 +361,11 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
// Transfer tokens to contract
|
||||
require(aitbcToken.transferFrom(msg.sender, address(this), _amount), "Transfer failed");
|
||||
|
||||
// Update rate limiting counters
|
||||
userStakeCount[msg.sender]++;
|
||||
dailyStakeCount[msg.sender]++;
|
||||
lastStakeTime[msg.sender] = block.timestamp;
|
||||
|
||||
emit StakeCreated(stakeId, msg.sender, _agentWallet, _amount, _lockPeriod, apy);
|
||||
|
||||
return stakeId;
|
||||
@@ -421,10 +533,11 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Updates agent performance metrics and tier
|
||||
* @dev Updates agent performance metrics and tier (DEPRECATED - use updateAgentPerformanceWithSignature)
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _accuracy Latest accuracy score
|
||||
* @param _successful Whether the submission was successful
|
||||
* @dev This function is deprecated and will be removed in future version
|
||||
*/
|
||||
function updateAgentPerformance(
|
||||
address _agentWallet,
|
||||
@@ -433,40 +546,16 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
) external
|
||||
supportedAgent(_agentWallet)
|
||||
nonReentrant
|
||||
onlyAuthorizedOracle
|
||||
{
|
||||
AgentMetrics storage metrics = agentMetrics[_agentWallet];
|
||||
require(block.timestamp >= lastPerformanceUpdateTime[_agentWallet] + performanceUpdateDelay, "Update too frequent");
|
||||
|
||||
metrics.totalSubmissions++;
|
||||
if (_successful) {
|
||||
metrics.successfulSubmissions++;
|
||||
}
|
||||
lastPerformanceUpdateTime[_agentWallet] = block.timestamp;
|
||||
|
||||
// Update average accuracy (weighted average)
|
||||
uint256 totalAccuracy = metrics.averageAccuracy * (metrics.totalSubmissions - 1) + _accuracy;
|
||||
metrics.averageAccuracy = totalAccuracy / metrics.totalSubmissions;
|
||||
// Update reputation
|
||||
updateOracleReputation(msg.sender, true);
|
||||
|
||||
metrics.lastUpdateTime = block.timestamp;
|
||||
|
||||
// Calculate new tier
|
||||
PerformanceTier newTier = _calculateAgentTier(_agentWallet);
|
||||
PerformanceTier oldTier = metrics.currentTier;
|
||||
|
||||
if (newTier != oldTier) {
|
||||
metrics.currentTier = newTier;
|
||||
|
||||
// Update APY for all active stakes on this agent
|
||||
uint256[] storage stakesForAgent = agentStakes[_agentWallet];
|
||||
for (uint256 i = 0; i < stakesForAgent.length; i++) {
|
||||
uint256 stakeId = stakesForAgent[i];
|
||||
Stake storage stake = stakes[stakeId];
|
||||
if (stake.status == StakeStatus.ACTIVE) {
|
||||
stake.currentAPY = _calculateAPY(_agentWallet, stake.lockPeriod, newTier);
|
||||
stake.agentTier = newTier;
|
||||
}
|
||||
}
|
||||
|
||||
emit AgentTierUpdated(_agentWallet, oldTier, newTier, metrics.tierScore);
|
||||
}
|
||||
_updateAgentPerformanceInternal(_agentWallet, _accuracy, _successful);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -824,4 +913,426 @@ contract AgentStaking is Ownable, ReentrancyGuard, Pausable {
|
||||
function unpause() external onlyOwner {
|
||||
_unpause();
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Slashing Mechanism
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* @dev Manually slash a stake (owner only)
|
||||
* @param _stakeId Stake ID to slash
|
||||
* @param _slashingPercentage Percentage to slash (0-100)
|
||||
* @param _reason Reason for slashing
|
||||
*/
|
||||
function slashStake(
|
||||
uint256 _stakeId,
|
||||
uint256 _slashingPercentage,
|
||||
string memory _reason
|
||||
) external onlyOwner {
|
||||
Stake storage stake = stakes[_stakeId];
|
||||
require(stake.status == StakeStatus.ACTIVE, "Stake not active");
|
||||
require(_slashingPercentage <= 100, "Invalid percentage");
|
||||
require(stake.amount > 0, "No amount to slash");
|
||||
|
||||
uint256 slashAmount = (stake.amount * _slashingPercentage) / 100;
|
||||
stake.amount -= slashAmount;
|
||||
stake.status = StakeStatus.SLASHED;
|
||||
|
||||
require(aitbcToken.transfer(owner(), slashAmount), "Transfer failed");
|
||||
|
||||
emit StakeSlashed(_stakeId, stake.staker, slashAmount, _reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check and slash agent based on performance metrics
|
||||
* @param _agentWallet Agent wallet address
|
||||
*/
|
||||
function checkAndSlashAgent(address _agentWallet) external onlyOwner {
|
||||
require(agentMetrics[_agentWallet].agentWallet != address(0), "Agent not found");
|
||||
|
||||
AgentMetrics storage metrics = agentMetrics[_agentWallet];
|
||||
|
||||
SlashingCondition storage conditions = slashingConditions[_agentWallet];
|
||||
uint256 minAccuracy = conditions.minAccuracyThreshold > 0 ? conditions.minAccuracyThreshold : defaultMinAccuracy;
|
||||
uint256 maxMissed = conditions.maxMissedJobs > 0 ? conditions.maxMissedJobs : defaultMaxMissedJobs;
|
||||
uint256 slashPct = conditions.slashingPercentage > 0 ? conditions.slashingPercentage : defaultSlashingPercentage;
|
||||
|
||||
if (metrics.averageAccuracy < minAccuracy) {
|
||||
_slashAllStakesForAgent(_agentWallet, slashPct, "Low accuracy");
|
||||
return;
|
||||
}
|
||||
|
||||
uint256 missedJobs = metrics.totalSubmissions - metrics.successfulSubmissions;
|
||||
if (missedJobs > maxMissed) {
|
||||
_slashAllStakesForAgent(_agentWallet, slashPct, "Too many missed jobs");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to slash all stakes for an agent
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _slashingPercentage Percentage to slash
|
||||
* @param _reason Reason for slashing
|
||||
*/
|
||||
function _slashAllStakesForAgent(
|
||||
address _agentWallet,
|
||||
uint256 _slashingPercentage,
|
||||
string memory _reason
|
||||
) internal {
|
||||
uint256[] storage stakesForAgent = agentStakes[_agentWallet];
|
||||
|
||||
for (uint256 i = 0; i < stakesForAgent.length; i++) {
|
||||
uint256 stakeId = stakesForAgent[i];
|
||||
Stake storage stake = stakes[stakeId];
|
||||
|
||||
if (stake.status == StakeStatus.ACTIVE && stake.amount > 0) {
|
||||
uint256 slashAmount = (stake.amount * _slashingPercentage) / 100;
|
||||
stake.amount -= slashAmount;
|
||||
stake.status = StakeStatus.SLASHED;
|
||||
|
||||
require(aitbcToken.transfer(owner(), slashAmount), "Transfer failed");
|
||||
|
||||
emit StakeSlashed(stakeId, stake.staker, slashAmount, _reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev File an appeal for a slashed stake
|
||||
* @param _stakeId Stake ID to appeal
|
||||
* @param _reason Reason for appeal
|
||||
*/
|
||||
function appealSlashing(uint256 _stakeId, string memory _reason) external {
|
||||
Stake storage stake = stakes[_stakeId];
|
||||
require(stake.staker == msg.sender, "Not your stake");
|
||||
require(stake.status == StakeStatus.SLASHED, "Not slashed");
|
||||
require(block.timestamp - stake.lastRewardTime < appealWindow, "Appeal window expired");
|
||||
require(slashAppeals[_stakeId].appellant == address(0), "Appeal already filed");
|
||||
|
||||
slashAppeals[_stakeId] = SlashAppeal({
|
||||
stakeId: _stakeId,
|
||||
appellant: msg.sender,
|
||||
reason: _reason,
|
||||
appealTime: block.timestamp,
|
||||
resolved: false,
|
||||
approved: false
|
||||
});
|
||||
|
||||
emit SlashAppealFiled(_stakeId, msg.sender, _reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Resolve a slash appeal (owner only)
|
||||
* @param _stakeId Stake ID
|
||||
* @param _approved Whether to approve the appeal
|
||||
*/
|
||||
function resolveSlashAppeal(uint256 _stakeId, bool _approved) external onlyOwner {
|
||||
SlashAppeal storage appeal = slashAppeals[_stakeId];
|
||||
require(appeal.appellant != address(0), "No appeal found");
|
||||
require(!appeal.resolved, "Already resolved");
|
||||
|
||||
appeal.resolved = true;
|
||||
appeal.approved = _approved;
|
||||
|
||||
if (_approved) {
|
||||
Stake storage stake = stakes[_stakeId];
|
||||
stake.status = StakeStatus.ACTIVE;
|
||||
emit SlashAppealApproved(_stakeId);
|
||||
} else {
|
||||
emit SlashAppealRejected(_stakeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Report a malicious agent and earn reward
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _evidence Evidence of malicious behavior
|
||||
*/
|
||||
function reportMaliciousAgent(
|
||||
address _agentWallet,
|
||||
string memory _evidence
|
||||
) external {
|
||||
require(agentMetrics[_agentWallet].agentWallet != address(0), "Agent not found");
|
||||
|
||||
AgentMetrics storage metrics = agentMetrics[_agentWallet];
|
||||
|
||||
SlashingCondition storage conditions = slashingConditions[_agentWallet];
|
||||
uint256 minAccuracy = conditions.minAccuracyThreshold > 0 ? conditions.minAccuracyThreshold : defaultMinAccuracy;
|
||||
uint256 slashPct = conditions.slashingPercentage > 0 ? conditions.slashingPercentage : defaultSlashingPercentage;
|
||||
|
||||
if (metrics.averageAccuracy < minAccuracy) {
|
||||
_slashAllStakesForAgent(_agentWallet, slashPct, string(abi.encodePacked("Reporter: ", _evidence)));
|
||||
|
||||
uint256 totalSlashed = _calculateTotalSlashed(_agentWallet);
|
||||
uint256 reward = (totalSlashed * slashReporterReward) / 10000;
|
||||
|
||||
if (reward > 0) {
|
||||
require(aitbcToken.transfer(msg.sender, reward), "Reward transfer failed");
|
||||
}
|
||||
|
||||
emit MaliciousAgentReported(_agentWallet, msg.sender, reward);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set slashing conditions for an agent
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _minAccuracy Minimum accuracy threshold
|
||||
* @param _maxMissedJobs Maximum missed jobs
|
||||
* @param _slashingPercentage Slashing percentage
|
||||
*/
|
||||
function setSlashingConditions(
|
||||
address _agentWallet,
|
||||
uint256 _minAccuracy,
|
||||
uint256 _maxMissedJobs,
|
||||
uint256 _slashingPercentage
|
||||
) external onlyOwner {
|
||||
require(_agentWallet != address(0), "Invalid agent address");
|
||||
require(_minAccuracy <= 100, "Invalid accuracy threshold");
|
||||
require(_slashingPercentage <= 100, "Invalid slash percentage");
|
||||
|
||||
slashingConditions[_agentWallet] = SlashingCondition({
|
||||
minAccuracyThreshold: _minAccuracy,
|
||||
maxMissedJobs: _maxMissedJobs,
|
||||
slashingPercentage: _slashingPercentage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Calculate total slashed amount for an agent
|
||||
* @param _agentWallet Agent wallet address
|
||||
*/
|
||||
function _calculateTotalSlashed(address _agentWallet) internal view returns (uint256) {
|
||||
uint256[] storage stakesForAgent = agentStakes[_agentWallet];
|
||||
uint256 totalSlashed = 0;
|
||||
|
||||
for (uint256 i = 0; i < stakesForAgent.length; i++) {
|
||||
uint256 stakeId = stakesForAgent[i];
|
||||
Stake storage stake = stakes[stakeId];
|
||||
|
||||
if (stake.status == StakeStatus.SLASHED) {
|
||||
totalSlashed += (stake.amount * defaultSlashingPercentage) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSlashed;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Oracle Protection
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* @dev Add an authorized oracle (owner only)
|
||||
* @param _oracle Oracle address to add
|
||||
*/
|
||||
function addOracle(address _oracle) external onlyOwner {
|
||||
require(_oracle != address(0), "Invalid oracle address");
|
||||
require(!authorizedOracles[_oracle], "Oracle already authorized");
|
||||
|
||||
authorizedOracles[_oracle] = true;
|
||||
oracleList.push(_oracle);
|
||||
oracleCount++;
|
||||
|
||||
emit OracleAdded(_oracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Remove an authorized oracle (owner only)
|
||||
* @param _oracle Oracle address to remove
|
||||
*/
|
||||
function removeOracle(address _oracle) external onlyOwner {
|
||||
require(authorizedOracles[_oracle], "Oracle not authorized");
|
||||
|
||||
authorizedOracles[_oracle] = false;
|
||||
oracleCount--;
|
||||
|
||||
emit OracleRemoved(_oracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update agent performance with oracle signature verification
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _accuracy Accuracy score
|
||||
* @param _successful Whether submission was successful
|
||||
* @param _timestamp Timestamp of update
|
||||
* @param _nonce Oracle nonce
|
||||
* @param _signature Oracle signature
|
||||
*/
|
||||
function updateAgentPerformanceWithSignature(
|
||||
address _agentWallet,
|
||||
uint256 _accuracy,
|
||||
bool _successful,
|
||||
uint256 _timestamp,
|
||||
uint256 _nonce,
|
||||
bytes memory _signature
|
||||
) external onlyAuthorizedOracle {
|
||||
require(block.timestamp <= _timestamp + 1 hours, "Signature expired");
|
||||
require(oracleNonces[msg.sender] == _nonce, "Invalid nonce");
|
||||
|
||||
// Verify signature using ECDSA library
|
||||
bytes32 messageHash = keccak256(abi.encodePacked(_agentWallet, _accuracy, _successful, _timestamp, _nonce));
|
||||
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
|
||||
address signer = ECDSA.recover(ethSignedMessageHash, _signature);
|
||||
require(signer == msg.sender, "Invalid signature");
|
||||
|
||||
// Update nonce
|
||||
oracleNonces[msg.sender]++;
|
||||
|
||||
// Update reputation
|
||||
updateOracleReputation(msg.sender, true);
|
||||
|
||||
// Call internal update function
|
||||
_updateAgentPerformanceInternal(_agentWallet, _accuracy, _successful);
|
||||
|
||||
emit PerformanceUpdateWithSignature(msg.sender, _agentWallet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to update agent performance metrics
|
||||
* @param _agentWallet Agent wallet address
|
||||
* @param _accuracy Accuracy score
|
||||
* @param _successful Whether submission was successful
|
||||
*/
|
||||
function _updateAgentPerformanceInternal(
|
||||
address _agentWallet,
|
||||
uint256 _accuracy,
|
||||
bool _successful
|
||||
) internal {
|
||||
AgentMetrics storage metrics = agentMetrics[_agentWallet];
|
||||
|
||||
metrics.totalSubmissions++;
|
||||
if (_successful) {
|
||||
metrics.successfulSubmissions++;
|
||||
}
|
||||
|
||||
// Update average accuracy (weighted average)
|
||||
uint256 totalAccuracy = metrics.averageAccuracy * (metrics.totalSubmissions - 1) + _accuracy;
|
||||
metrics.averageAccuracy = totalAccuracy / metrics.totalSubmissions;
|
||||
|
||||
metrics.lastUpdateTime = block.timestamp;
|
||||
|
||||
// Calculate new tier
|
||||
PerformanceTier newTier = _calculateAgentTier(_agentWallet);
|
||||
PerformanceTier oldTier = metrics.currentTier;
|
||||
|
||||
if (newTier != oldTier) {
|
||||
metrics.currentTier = newTier;
|
||||
|
||||
// Update APY for all active stakes on this agent
|
||||
uint256[] storage stakesForAgent = agentStakes[_agentWallet];
|
||||
for (uint256 i = 0; i < stakesForAgent.length; i++) {
|
||||
uint256 stakeId = stakesForAgent[i];
|
||||
Stake storage stake = stakes[stakeId];
|
||||
if (stake.status == StakeStatus.ACTIVE) {
|
||||
stake.currentAPY = _calculateAPY(_agentWallet, stake.lockPeriod, newTier);
|
||||
stake.agentTier = newTier;
|
||||
}
|
||||
}
|
||||
|
||||
emit AgentTierUpdated(_agentWallet, oldTier, newTier, metrics.tierScore);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Rotate an oracle (owner only)
|
||||
* @param _oldOracle Old oracle address
|
||||
* @param _newOracle New oracle address
|
||||
*/
|
||||
function rotateOracle(address _oldOracle, address _newOracle) external onlyOwner {
|
||||
require(authorizedOracles[_oldOracle], "Old oracle not authorized");
|
||||
require(!authorizedOracles[_newOracle], "New oracle already authorized");
|
||||
require(block.timestamp >= lastOracleRotation + oracleRotationPeriod, "Rotation too soon");
|
||||
|
||||
authorizedOracles[_oldOracle] = false;
|
||||
authorizedOracles[_newOracle] = true;
|
||||
lastOracleRotation = block.timestamp;
|
||||
|
||||
emit OracleRotated(_oldOracle, _newOracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Update oracle reputation
|
||||
* @param _oracle Oracle address
|
||||
* @param _successful Whether the update was successful
|
||||
*/
|
||||
function updateOracleReputation(address _oracle, bool _successful) internal {
|
||||
OracleReputation storage rep = oracleReputations[_oracle];
|
||||
rep.totalUpdates++;
|
||||
|
||||
if (_successful) {
|
||||
rep.successfulUpdates++;
|
||||
rep.reputationScore = (rep.successfulUpdates * 100) / rep.totalUpdates;
|
||||
} else {
|
||||
rep.disputedUpdates++;
|
||||
rep.reputationScore = (rep.successfulUpdates * 100) / rep.totalUpdates;
|
||||
|
||||
// Remove oracle if reputation falls below threshold
|
||||
if (rep.reputationScore < 50 && rep.totalUpdates >= 10) {
|
||||
authorizedOracles[_oracle] = false;
|
||||
emit OracleRemovedForLowReputation(_oracle, rep.reputationScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Report a disputed oracle update
|
||||
* @param _oracle Oracle address
|
||||
* @param _evidence Evidence of dispute
|
||||
*/
|
||||
function reportDisputedOracle(address _oracle, string memory _evidence) external onlyOwner {
|
||||
require(authorizedOracles[_oracle], "Oracle not authorized");
|
||||
|
||||
updateOracleReputation(_oracle, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set performance update delay (owner only)
|
||||
* @param _delay New delay in seconds
|
||||
*/
|
||||
function setPerformanceUpdateDelay(uint256 _delay) external onlyOwner {
|
||||
performanceUpdateDelay = _delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set oracle rotation period (owner only)
|
||||
* @param _period New period in seconds
|
||||
*/
|
||||
function setOracleRotationPeriod(uint256 _period) external onlyOwner {
|
||||
oracleRotationPeriod = _period;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Rate Limiting Admin Functions
|
||||
// =========================
|
||||
|
||||
/**
|
||||
* @dev Set max stakes per day (owner only)
|
||||
* @param _maxStakes New maximum stakes per day
|
||||
*/
|
||||
function setMaxStakesPerDay(uint256 _maxStakes) external onlyOwner {
|
||||
require(_maxStakes >= 1, "Must allow at least 1 stake per day");
|
||||
require(_maxStakes <= 100, "Cannot exceed 100 stakes per day");
|
||||
maxStakesPerDay = _maxStakes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set max stakes per user (owner only)
|
||||
* @param _maxStakes New maximum total stakes per user
|
||||
*/
|
||||
function setMaxStakesPerUser(uint256 _maxStakes) external onlyOwner {
|
||||
require(_maxStakes >= 1, "Must allow at least 1 stake per user");
|
||||
require(_maxStakes <= 500, "Cannot exceed 500 stakes per user");
|
||||
maxStakesPerUser = _maxStakes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set stake cooldown (owner only)
|
||||
* @param _cooldown New cooldown period in seconds
|
||||
*/
|
||||
function setStakeCooldown(uint256 _cooldown) external onlyOwner {
|
||||
require(_cooldown >= 0, "Cooldown cannot be negative");
|
||||
require(_cooldown <= 1 hours, "Cooldown too long");
|
||||
stakeCooldown = _cooldown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,16 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 public defaultReleaseDelay = 3600; // 1 hour default
|
||||
uint256 public emergencyReleaseDelay = 86400; // 24 hours for emergency
|
||||
uint256 public platformFeePercentage = 50; // 0.5% in basis points
|
||||
uint256 public constant BASIS_POINTS = 10000; // 100% in basis points
|
||||
|
||||
// Multi-oracle verification state variables
|
||||
uint256 public oracleVerificationThreshold = 2; // Minimum oracles required
|
||||
uint256 public oracleVerificationDelay = 1 hours; // Delay after verification before release
|
||||
|
||||
// Emergency release voting threshold state variables
|
||||
uint256 public emergencyReleaseVotingThreshold = 66; // 66% approval threshold (in basis points)
|
||||
uint256 public emergencyReleaseQuorum = 3; // Minimum arbiters required to vote
|
||||
uint256 public emergencyReleaseTimelock = 1 hours; // Time lock after approval before execution
|
||||
|
||||
// Structs
|
||||
struct EscrowAccount {
|
||||
@@ -58,8 +68,17 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 verificationTime;
|
||||
string conditionData;
|
||||
uint256 confidence;
|
||||
// Multi-oracle verification fields (without nested mappings)
|
||||
address[] assignedOracles;
|
||||
uint256 verificationCount;
|
||||
uint256 requiredVerifications;
|
||||
uint256 finalVerificationTime;
|
||||
}
|
||||
|
||||
// Separate mappings for multi-oracle verification (to avoid nested mappings in struct)
|
||||
mapping(uint256 => mapping(address => bool)) public oracleHasVerified;
|
||||
mapping(uint256 => mapping(address => bool)) public oracleVerdict;
|
||||
|
||||
struct MultiSigRelease {
|
||||
uint256 escrowId;
|
||||
address[] requiredSigners;
|
||||
@@ -92,6 +111,7 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 totalVotes;
|
||||
bool isApproved;
|
||||
bool isExecuted;
|
||||
uint256 approvalTime; // Timestamp when approval was achieved
|
||||
}
|
||||
|
||||
// Enums
|
||||
@@ -240,6 +260,21 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
address indexed collector
|
||||
);
|
||||
|
||||
// Multi-oracle verification events
|
||||
event OracleVerificationSubmitted(
|
||||
uint256 indexed escrowId,
|
||||
address indexed oracle,
|
||||
bool verdict,
|
||||
uint256 confidence
|
||||
);
|
||||
event OracleVerificationThresholdMet(
|
||||
uint256 indexed escrowId,
|
||||
uint256 verificationCount,
|
||||
uint256 requiredVerifications
|
||||
);
|
||||
event OracleAuthorized(address indexed oracle);
|
||||
event OracleRevoked(address indexed oracle);
|
||||
|
||||
// Modifiers
|
||||
modifier onlyAuthorizedOracle() {
|
||||
require(authorizedOracles[msg.sender], "Not authorized oracle");
|
||||
@@ -407,15 +442,24 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
EscrowAccount storage escrow = escrowAccounts[_escrowId];
|
||||
escrow.conditionHash = _condition;
|
||||
|
||||
conditionalReleases[_escrowId] = ConditionalRelease({
|
||||
escrowId: _escrowId,
|
||||
condition: _condition,
|
||||
conditionMet: false,
|
||||
oracle: _oracle,
|
||||
verificationTime: 0,
|
||||
conditionData: _conditionData,
|
||||
confidence: 0
|
||||
});
|
||||
// Initialize ConditionalRelease with multi-oracle support
|
||||
ConditionalRelease storage condRelease = conditionalReleases[_escrowId];
|
||||
condRelease.escrowId = _escrowId;
|
||||
condRelease.condition = _condition;
|
||||
condRelease.conditionMet = false;
|
||||
condRelease.oracle = _oracle;
|
||||
condRelease.verificationTime = 0;
|
||||
condRelease.conditionData = _conditionData;
|
||||
condRelease.confidence = 0;
|
||||
// Initialize multi-oracle fields
|
||||
condRelease.verificationCount = 0;
|
||||
condRelease.requiredVerifications = oracleVerificationThreshold;
|
||||
condRelease.finalVerificationTime = 0;
|
||||
|
||||
// If a single oracle is specified, add to assigned oracles
|
||||
if (_oracle != address(0)) {
|
||||
condRelease.assignedOracles.push(_oracle);
|
||||
}
|
||||
|
||||
conditionEscrows[_condition] = _escrowId;
|
||||
|
||||
@@ -434,16 +478,54 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
uint256 _confidence
|
||||
) external onlyAuthorizedOracle escrowExists(_escrowId) escrowNotFrozen(_escrowId) escrowNotReleased(_escrowId) {
|
||||
ConditionalRelease storage condRelease = conditionalReleases[_escrowId];
|
||||
require(condRelease.oracle == msg.sender, "Not assigned oracle");
|
||||
|
||||
condRelease.conditionMet = _conditionMet;
|
||||
condRelease.verificationTime = block.timestamp;
|
||||
condRelease.confidence = _confidence;
|
||||
// Check if oracle is assigned (for backward compatibility with single oracle mode)
|
||||
if (condRelease.assignedOracles.length == 0) {
|
||||
require(condRelease.oracle == msg.sender, "Not assigned oracle");
|
||||
|
||||
condRelease.conditionMet = _conditionMet;
|
||||
condRelease.verificationTime = block.timestamp;
|
||||
condRelease.confidence = _confidence;
|
||||
|
||||
emit ConditionMet(_escrowId, condRelease.condition, _conditionMet, block.timestamp);
|
||||
|
||||
if (_conditionMet) {
|
||||
_releaseEscrow(_escrowId, "Condition verified and met");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
emit ConditionMet(_escrowId, condRelease.condition, _conditionMet, block.timestamp);
|
||||
// Multi-oracle verification mode
|
||||
require(!oracleHasVerified[_escrowId][msg.sender], "Oracle already verified");
|
||||
|
||||
if (_conditionMet) {
|
||||
_releaseEscrow(_escrowId, "Condition verified and met");
|
||||
oracleHasVerified[_escrowId][msg.sender] = true;
|
||||
oracleVerdict[_escrowId][msg.sender] = _conditionMet;
|
||||
condRelease.verificationCount++;
|
||||
|
||||
emit OracleVerificationSubmitted(_escrowId, msg.sender, _conditionMet, _confidence);
|
||||
|
||||
// Check if threshold is met
|
||||
if (condRelease.verificationCount >= condRelease.requiredVerifications) {
|
||||
// Count positive verdicts
|
||||
uint256 positiveVotes = 0;
|
||||
for (uint256 i = 0; i < condRelease.assignedOracles.length; i++) {
|
||||
if (oracleVerdict[_escrowId][condRelease.assignedOracles[i]]) {
|
||||
positiveVotes++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if majority approves
|
||||
if (positiveVotes > condRelease.assignedOracles.length / 2) {
|
||||
condRelease.conditionMet = true;
|
||||
condRelease.finalVerificationTime = block.timestamp;
|
||||
|
||||
emit OracleVerificationThresholdMet(_escrowId, condRelease.verificationCount, condRelease.requiredVerifications);
|
||||
|
||||
// Apply time delay before release
|
||||
if (block.timestamp >= condRelease.finalVerificationTime + oracleVerificationDelay) {
|
||||
_releaseEscrow(_escrowId, "Multi-oracle verification completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,10 +691,20 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
}
|
||||
|
||||
// Check if voting is complete and approved
|
||||
if (emergency.totalVotes >= 3 && emergency.votesFor > emergency.votesAgainst) {
|
||||
emergency.isApproved = true;
|
||||
emit EmergencyReleaseApproved(_escrowId, emergency.votesFor, emergency.votesAgainst, true);
|
||||
_releaseEscrow(_escrowId, "Emergency release approved");
|
||||
if (emergency.totalVotes >= emergencyReleaseQuorum) {
|
||||
// Calculate approval percentage
|
||||
uint256 approvalPercentage = (emergency.votesFor * BASIS_POINTS) / emergency.totalVotes;
|
||||
|
||||
if (approvalPercentage >= emergencyReleaseVotingThreshold) {
|
||||
emergency.isApproved = true;
|
||||
emergency.approvalTime = block.timestamp;
|
||||
emit EmergencyReleaseApproved(_escrowId, emergency.votesFor, emergency.votesAgainst, true);
|
||||
|
||||
// Apply timelock before execution
|
||||
if (block.timestamp >= emergency.approvalTime + emergencyReleaseTimelock) {
|
||||
_releaseEscrow(_escrowId, "Emergency release approved and timelock elapsed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,6 +764,80 @@ contract EscrowService is Ownable, ReentrancyGuard, Pausable {
|
||||
*/
|
||||
function revokeOracle(address _oracle) external onlyOwner {
|
||||
authorizedOracles[_oracle] = false;
|
||||
emit OracleRevoked(_oracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets the oracle verification threshold
|
||||
* @param newThreshold Minimum oracles required for verification
|
||||
*/
|
||||
function setOracleVerificationThreshold(uint256 newThreshold) external onlyOwner {
|
||||
require(newThreshold >= 1, "Threshold must be at least 1");
|
||||
require(newThreshold <= 10, "Threshold too high");
|
||||
oracleVerificationThreshold = newThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets the oracle verification delay
|
||||
* @param newDelay Delay after verification before release
|
||||
*/
|
||||
function setOracleVerificationDelay(uint256 newDelay) external onlyOwner {
|
||||
require(newDelay >= 0, "Delay cannot be negative");
|
||||
require(newDelay <= 24 hours, "Delay too long");
|
||||
oracleVerificationDelay = newDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Assigns multiple oracles to a conditional release
|
||||
* @param _escrowId ID of the escrow
|
||||
* @param _oracles Array of oracle addresses
|
||||
*/
|
||||
function assignMultipleOracles(uint256 _escrowId, address[] memory _oracles) external onlyOwner escrowExists(_escrowId) {
|
||||
ConditionalRelease storage condRelease = conditionalReleases[_escrowId];
|
||||
|
||||
// Clear existing assigned oracles
|
||||
delete condRelease.assignedOracles;
|
||||
|
||||
// Assign new oracles
|
||||
for (uint256 i = 0; i < _oracles.length; i++) {
|
||||
require(authorizedOracles[_oracles[i]], "Unauthorized oracle");
|
||||
condRelease.assignedOracles.push(_oracles[i]);
|
||||
}
|
||||
|
||||
// Update required verifications based on oracle count
|
||||
condRelease.requiredVerifications = _oracles.length >= oracleVerificationThreshold
|
||||
? oracleVerificationThreshold
|
||||
: _oracles.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets the emergency release voting threshold
|
||||
* @param newThreshold Percentage threshold (in basis points)
|
||||
*/
|
||||
function setEmergencyReleaseVotingThreshold(uint256 newThreshold) external onlyOwner {
|
||||
require(newThreshold >= 51, "Threshold must be at least 51%");
|
||||
require(newThreshold <= 100, "Threshold cannot exceed 100%");
|
||||
emergencyReleaseVotingThreshold = newThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets the emergency release quorum
|
||||
* @param newQuorum Minimum arbiters required to vote
|
||||
*/
|
||||
function setEmergencyReleaseQuorum(uint256 newQuorum) external onlyOwner {
|
||||
require(newQuorum >= 1, "Quorum must be at least 1");
|
||||
require(newQuorum <= 10, "Quorum too high");
|
||||
emergencyReleaseQuorum = newQuorum;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets the emergency release timelock
|
||||
* @param newTimelock Time lock after approval before execution
|
||||
*/
|
||||
function setEmergencyReleaseTimelock(uint256 newTimelock) external onlyOwner {
|
||||
require(newTimelock >= 0, "Timelock cannot be negative");
|
||||
require(newTimelock <= 24 hours, "Timelock too long");
|
||||
emergencyReleaseTimelock = newTimelock;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
38
contracts/scripts/deploy_aitoken_staging.js
Normal file
38
contracts/scripts/deploy_aitoken_staging.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const hre = require("hardhat");
|
||||
|
||||
async function main() {
|
||||
console.log("Deploying AIToken to testnet...");
|
||||
|
||||
const [owner] = await hre.ethers.getSigners();
|
||||
console.log("Deploying from account:", owner.address);
|
||||
|
||||
const AIToken = await hre.ethers.getContractFactory("AIToken");
|
||||
const initialSupply = hre.ethers.parseEther("1000000"); // 1 million for staging
|
||||
const token = await AIToken.deploy(initialSupply);
|
||||
|
||||
await token.waitForDeployment();
|
||||
const tokenAddress = await token.getAddress();
|
||||
|
||||
console.log("AIToken deployed to:", tokenAddress);
|
||||
|
||||
// Verify supply cap
|
||||
const MAX_SUPPLY = await token.MAX_SUPPLY();
|
||||
console.log("MAX_SUPPLY:", hre.ethers.formatEther(MAX_SUPPLY));
|
||||
|
||||
// Verify cooldown
|
||||
const COOLDOWN = await token.MINTING_COOLDOWN();
|
||||
console.log("MINTING_COOLDOWN:", COOLDOWN.toString());
|
||||
|
||||
// Verify initial supply
|
||||
const totalSupply = await token.totalSupply();
|
||||
console.log("Total Supply:", hre.ethers.formatEther(totalSupply));
|
||||
|
||||
console.log("\nDeployment successful!");
|
||||
console.log("Token Address:", tokenAddress);
|
||||
console.log("Owner Address:", owner.address);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
500
contracts/test/AgentStakingSecurity.test.js
Normal file
500
contracts/test/AgentStakingSecurity.test.js
Normal file
@@ -0,0 +1,500 @@
|
||||
import { expect } from "chai";
|
||||
import hardhat from "hardhat";
|
||||
const { ethers } = hardhat;
|
||||
|
||||
describe("AgentStaking Security Tests", function () {
|
||||
let agentStaking;
|
||||
let aitbcToken;
|
||||
let owner, oracle, staker, agentWallet, attacker;
|
||||
|
||||
beforeEach(async function () {
|
||||
[owner, oracle, staker, agentWallet, attacker] = await ethers.getSigners();
|
||||
|
||||
// Deploy mock AIToken
|
||||
const AIToken = await ethers.getContractFactory("AIToken");
|
||||
aitbcToken = await AIToken.deploy(ethers.parseEther("1000000"));
|
||||
await aitbcToken.waitForDeployment();
|
||||
|
||||
// Transfer tokens to staker
|
||||
await aitbcToken.transfer(staker.address, ethers.parseEther("10000"));
|
||||
await aitbcToken.transfer(attacker.address, ethers.parseEther("10000"));
|
||||
|
||||
// Deploy AgentStaking
|
||||
const AgentStaking = await ethers.getContractFactory("AgentStaking");
|
||||
agentStaking = await AgentStaking.deploy(
|
||||
await aitbcToken.getAddress(),
|
||||
ethers.ZeroAddress // PerformanceVerifier (not needed for these tests)
|
||||
);
|
||||
await agentStaking.waitForDeployment();
|
||||
|
||||
// Add supported agent
|
||||
await agentStaking.addSupportedAgent(
|
||||
agentWallet.address,
|
||||
2 // SILVER tier
|
||||
);
|
||||
|
||||
// Add oracle
|
||||
await agentStaking.addOracle(oracle.address);
|
||||
});
|
||||
|
||||
describe("SC-H-01: Slashing Mechanism", function () {
|
||||
beforeEach(async function () {
|
||||
// Stake tokens
|
||||
await aitbcToken.connect(staker).approve(
|
||||
await agentStaking.getAddress(),
|
||||
ethers.parseEther("1000")
|
||||
);
|
||||
|
||||
await agentStaking.connect(staker).stakeOnAgent(
|
||||
agentWallet.address,
|
||||
ethers.parseEther("1000"),
|
||||
30 * 24 * 60 * 60,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow owner to manually slash a stake", async function () {
|
||||
const stakeId = 0;
|
||||
const slashingPercentage = 10;
|
||||
|
||||
await expect(
|
||||
agentStaking.slashStake(
|
||||
stakeId,
|
||||
slashingPercentage,
|
||||
"Manual slash for testing"
|
||||
)
|
||||
).to.emit(agentStaking, "StakeSlashed");
|
||||
|
||||
const stake = await agentStaking.stakes(stakeId);
|
||||
expect(stake.status).to.equal(3); // SLASHED
|
||||
expect(stake.amount).to.equal(ethers.parseEther("900")); // 1000 - 10%
|
||||
});
|
||||
|
||||
it("should not allow non-owner to slash", async function () {
|
||||
const stakeId = 0;
|
||||
const slashingPercentage = 10;
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(attacker).slashStake(
|
||||
stakeId,
|
||||
slashingPercentage,
|
||||
"Unauthorized slash"
|
||||
)
|
||||
).to.be.revertedWith("Ownable: caller is not the owner");
|
||||
});
|
||||
|
||||
it("should not allow slashing inactive stakes", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
// Fast forward past lock period (30 days)
|
||||
await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
// Unbond the stake first
|
||||
await agentStaking.connect(staker).unbondStake(stakeId);
|
||||
|
||||
await expect(
|
||||
agentStaking.slashStake(
|
||||
stakeId,
|
||||
10,
|
||||
"Test"
|
||||
)
|
||||
).to.be.revertedWith("Stake not active");
|
||||
});
|
||||
|
||||
it("should not allow invalid slashing percentage", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
await expect(
|
||||
agentStaking.slashStake(
|
||||
stakeId,
|
||||
101, // > 100%
|
||||
"Test"
|
||||
)
|
||||
).to.be.revertedWith("Invalid percentage");
|
||||
});
|
||||
|
||||
it("should automatically slash agent based on low accuracy", async function () {
|
||||
// Set low accuracy metrics (as oracle)
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
30, // 30% accuracy (below default 50%)
|
||||
false
|
||||
);
|
||||
|
||||
await expect(
|
||||
agentStaking.checkAndSlashAgent(agentWallet.address)
|
||||
).to.emit(agentStaking, "StakeSlashed");
|
||||
});
|
||||
|
||||
it("should automatically slash agent based on missed jobs", async function () {
|
||||
// Set metrics with many missed jobs (as oracle) with delays
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
70,
|
||||
false
|
||||
);
|
||||
|
||||
await expect(
|
||||
agentStaking.checkAndSlashAgent(agentWallet.address)
|
||||
).to.emit(agentStaking, "StakeSlashed");
|
||||
});
|
||||
|
||||
it("should allow filing an appeal for slashed stake", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
// Slash the stake
|
||||
await agentStaking.slashStake(stakeId, 10, "Test");
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(staker).appealSlashing(
|
||||
stakeId,
|
||||
"Appeal reason"
|
||||
)
|
||||
).to.emit(agentStaking, "SlashAppealFiled");
|
||||
});
|
||||
|
||||
it("should not allow appeal from non-staker", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
await agentStaking.slashStake(stakeId, 10, "Test");
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(attacker).appealSlashing(
|
||||
stakeId,
|
||||
"Appeal reason"
|
||||
)
|
||||
).to.be.revertedWith("Not your stake");
|
||||
});
|
||||
|
||||
it("should not allow appeal after window expires", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
await agentStaking.slashStake(stakeId, 10, "Test");
|
||||
|
||||
// Fast forward past appeal window (3 days)
|
||||
await ethers.provider.send("evm_increaseTime", [3 * 24 * 60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(staker).appealSlashing(
|
||||
stakeId,
|
||||
"Appeal reason"
|
||||
)
|
||||
).to.be.revertedWith("Appeal window expired");
|
||||
});
|
||||
|
||||
it("should allow owner to approve appeal", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
await agentStaking.slashStake(stakeId, 10, "Test");
|
||||
await agentStaking.connect(staker).appealSlashing(stakeId, "Appeal reason");
|
||||
|
||||
await expect(
|
||||
agentStaking.resolveSlashAppeal(stakeId, true)
|
||||
).to.emit(agentStaking, "SlashAppealApproved");
|
||||
|
||||
const stake = await agentStaking.stakes(stakeId);
|
||||
expect(stake.status).to.equal(0); // ACTIVE
|
||||
});
|
||||
|
||||
it("should allow owner to reject appeal", async function () {
|
||||
const stakeId = 0;
|
||||
|
||||
await agentStaking.slashStake(stakeId, 10, "Test");
|
||||
await agentStaking.connect(staker).appealSlashing(stakeId, "Appeal reason");
|
||||
|
||||
await expect(
|
||||
agentStaking.resolveSlashAppeal(stakeId, false)
|
||||
).to.emit(agentStaking, "SlashAppealRejected");
|
||||
});
|
||||
|
||||
it("should allow reporting malicious agent with reward", async function () {
|
||||
// Set low accuracy (as oracle)
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
30,
|
||||
false
|
||||
);
|
||||
|
||||
const reporterBalanceBefore = await aitbcToken.balanceOf(attacker.address);
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(attacker).reportMaliciousAgent(
|
||||
agentWallet.address,
|
||||
"Evidence of malicious behavior"
|
||||
)
|
||||
).to.emit(agentStaking, "MaliciousAgentReported");
|
||||
|
||||
const reporterBalanceAfter = await aitbcToken.balanceOf(attacker.address);
|
||||
expect(reporterBalanceAfter).to.be.gt(reporterBalanceBefore);
|
||||
});
|
||||
|
||||
it("should allow owner to set custom slashing conditions", async function () {
|
||||
await agentStaking.setSlashingConditions(
|
||||
agentWallet.address,
|
||||
60, // minAccuracy
|
||||
3, // maxMissedJobs
|
||||
15 // slashingPercentage
|
||||
);
|
||||
|
||||
const conditions = await agentStaking.slashingConditions(agentWallet.address);
|
||||
expect(conditions.minAccuracyThreshold).to.equal(60);
|
||||
expect(conditions.maxMissedJobs).to.equal(3);
|
||||
expect(conditions.slashingPercentage).to.equal(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SC-H-02: Oracle Protection", function () {
|
||||
it("should allow owner to add oracle", async function () {
|
||||
const newOracle = attacker;
|
||||
|
||||
await expect(
|
||||
agentStaking.addOracle(newOracle.address)
|
||||
).to.emit(agentStaking, "OracleAdded");
|
||||
|
||||
const isAuthorized = await agentStaking.authorizedOracles(newOracle.address);
|
||||
expect(isAuthorized).to.be.true;
|
||||
});
|
||||
|
||||
it("should not allow adding duplicate oracle", async function () {
|
||||
await expect(
|
||||
agentStaking.addOracle(oracle.address)
|
||||
).to.be.revertedWith("Oracle already authorized");
|
||||
});
|
||||
|
||||
it("should allow owner to remove oracle", async function () {
|
||||
await expect(
|
||||
agentStaking.removeOracle(oracle.address)
|
||||
).to.emit(agentStaking, "OracleRemoved");
|
||||
|
||||
const isAuthorized = await agentStaking.authorizedOracles(oracle.address);
|
||||
expect(isAuthorized).to.be.false;
|
||||
});
|
||||
|
||||
it("should not allow non-owner to add oracle", async function () {
|
||||
await expect(
|
||||
agentStaking.connect(attacker).addOracle(attacker.address)
|
||||
).to.be.revertedWith("Ownable: caller is not the owner");
|
||||
});
|
||||
|
||||
it("should not allow unauthorized oracle to update performance", async function () {
|
||||
await expect(
|
||||
agentStaking.connect(attacker).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
80,
|
||||
true
|
||||
)
|
||||
).to.be.revertedWith("Not authorized oracle");
|
||||
});
|
||||
|
||||
it("should allow authorized oracle to update performance with signature", async function () {
|
||||
const accuracy = 85;
|
||||
const successful = true;
|
||||
const nonce = await agentStaking.oracleNonces(oracle.address);
|
||||
|
||||
// Get current block timestamp
|
||||
const block = await ethers.provider.getBlock("latest");
|
||||
const timestamp = block.timestamp + 3600; // 1 hour in future
|
||||
|
||||
// Create message hash
|
||||
const messageHash = ethers.solidityPackedKeccak256(
|
||||
["address", "uint256", "bool", "uint256", "uint256"],
|
||||
[agentWallet.address, accuracy, successful, timestamp, nonce]
|
||||
);
|
||||
|
||||
// Sign the message hash directly (not the eth signed message hash)
|
||||
const signature = await oracle.signMessage(ethers.getBytes(messageHash));
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(oracle).updateAgentPerformanceWithSignature(
|
||||
agentWallet.address,
|
||||
accuracy,
|
||||
successful,
|
||||
timestamp,
|
||||
nonce,
|
||||
signature
|
||||
)
|
||||
).to.emit(agentStaking, "PerformanceUpdateWithSignature");
|
||||
});
|
||||
|
||||
it("should reject expired signature", async function () {
|
||||
const accuracy = 85;
|
||||
const successful = true;
|
||||
const timestamp = Math.floor(Date.now() / 1000) - 2 * 60 * 60; // 2 hours ago
|
||||
const nonce = await agentStaking.oracleNonces(oracle.address);
|
||||
|
||||
const messageHash = ethers.solidityPackedKeccak256(
|
||||
["address", "uint256", "bool", "uint256", "uint256"],
|
||||
[agentWallet.address, accuracy, successful, timestamp, nonce]
|
||||
);
|
||||
const ethSignedMessageHash = ethers.solidityPackedKeccak256(
|
||||
["string", "bytes32"],
|
||||
["\x19Ethereum Signed Message:\n32", messageHash]
|
||||
);
|
||||
const signature = await oracle.signMessage(ethers.getBytes(ethSignedMessageHash));
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(oracle).updateAgentPerformanceWithSignature(
|
||||
agentWallet.address,
|
||||
accuracy,
|
||||
successful,
|
||||
timestamp,
|
||||
nonce,
|
||||
signature
|
||||
)
|
||||
).to.be.revertedWith("Signature expired");
|
||||
});
|
||||
|
||||
it("should reject invalid nonce", async function () {
|
||||
const accuracy = 85;
|
||||
const successful = true;
|
||||
const nonce = BigInt(await agentStaking.oracleNonces(oracle.address)) + 1n;
|
||||
|
||||
// Get current block timestamp
|
||||
const block = await ethers.provider.getBlock("latest");
|
||||
const timestamp = block.timestamp + 3600; // 1 hour in future
|
||||
|
||||
const messageHash = ethers.solidityPackedKeccak256(
|
||||
["address", "uint256", "bool", "uint256", "uint256"],
|
||||
[agentWallet.address, accuracy, successful, timestamp, nonce]
|
||||
);
|
||||
const ethSignedMessageHash = ethers.solidityPackedKeccak256(
|
||||
["string", "bytes32"],
|
||||
["\x19Ethereum Signed Message:\n32", messageHash]
|
||||
);
|
||||
const signature = await oracle.signMessage(ethers.getBytes(ethSignedMessageHash));
|
||||
|
||||
await expect(
|
||||
agentStaking.connect(oracle).updateAgentPerformanceWithSignature(
|
||||
agentWallet.address,
|
||||
accuracy,
|
||||
successful,
|
||||
timestamp,
|
||||
nonce,
|
||||
signature
|
||||
)
|
||||
).to.be.revertedWith("Invalid nonce");
|
||||
});
|
||||
|
||||
it("should enforce time delay for performance updates", async function () {
|
||||
// First update should succeed
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
80,
|
||||
true
|
||||
);
|
||||
|
||||
// Immediate second update should fail
|
||||
await expect(
|
||||
agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
85,
|
||||
true
|
||||
)
|
||||
).to.be.revertedWith("Update too frequent");
|
||||
|
||||
// Fast forward past delay (1 hour)
|
||||
await ethers.provider.send("evm_increaseTime", [60 * 60 + 1]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
// Update after delay should succeed
|
||||
await expect(
|
||||
agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
85,
|
||||
true
|
||||
)
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it("should allow oracle rotation after period", async function () {
|
||||
const newOracle = attacker;
|
||||
|
||||
// First call should succeed (no rotation period set initially)
|
||||
await expect(
|
||||
agentStaking.rotateOracle(oracle.address, newOracle.address)
|
||||
).to.emit(agentStaking, "OracleRotated");
|
||||
});
|
||||
|
||||
it("should update oracle reputation on successful updates", async function () {
|
||||
await agentStaking.connect(oracle).updateAgentPerformance(
|
||||
agentWallet.address,
|
||||
80,
|
||||
true
|
||||
);
|
||||
|
||||
const reputation = await agentStaking.oracleReputations(oracle.address);
|
||||
expect(reputation.totalUpdates).to.equal(1);
|
||||
expect(reputation.successfulUpdates).to.equal(1);
|
||||
expect(reputation.reputationScore).to.equal(100);
|
||||
});
|
||||
|
||||
it("should allow owner to report disputed oracle", async function () {
|
||||
await expect(
|
||||
agentStaking.reportDisputedOracle(oracle.address, "Evidence")
|
||||
).to.not.be.reverted;
|
||||
|
||||
const reputation = await agentStaking.oracleReputations(oracle.address);
|
||||
expect(reputation.disputedUpdates).to.equal(1);
|
||||
});
|
||||
|
||||
it("should allow owner to set performance update delay", async function () {
|
||||
const newDelay = 2 * 60 * 60; // 2 hours
|
||||
|
||||
await agentStaking.setPerformanceUpdateDelay(newDelay);
|
||||
|
||||
const delay = await agentStaking.performanceUpdateDelay();
|
||||
expect(delay).to.equal(newDelay);
|
||||
});
|
||||
|
||||
it("should allow owner to set oracle rotation period", async function () {
|
||||
const newPeriod = 60 * 24 * 60 * 60; // 60 days
|
||||
|
||||
await agentStaking.setOracleRotationPeriod(newPeriod);
|
||||
|
||||
const period = await agentStaking.oracleRotationPeriod();
|
||||
expect(period).to.equal(newPeriod);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user