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

- 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:
aitbc
2026-05-11 13:46:42 +02:00
parent eeed0c61a3
commit e4f1a96172
141 changed files with 63860 additions and 2869 deletions

View File

@@ -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"
]
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
/**

View 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;
});

View 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);
});
});
});