Files
aitbc/contracts/test/AgentStakingSecurity.test.js
aitbc e4f1a96172
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
ci: standardize pytest invocation and add security scanning
- 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
2026-05-11 13:46:42 +02:00

501 lines
16 KiB
JavaScript

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