feat(tooling): add Phase 14 M4 - Developer Tooling
Adds formal verification DSL, multi-sig contract, and Hardhat plugin: synor-verifier crate: - Verification DSL for contract invariants and properties - SMT solver integration (Z3 backend optional) - Symbolic execution engine for path exploration - Automatic vulnerability detection (reentrancy, overflow, etc.) - 29 tests passing contracts/multi-sig: - M-of-N multi-signature wallet contract - Transaction proposals with timelock - Owner management (add/remove) - Emergency pause functionality - Native token and contract call support apps/hardhat-plugin (@synor/hardhat-plugin): - Network configuration for mainnet/testnet/devnet - Contract deployment with gas estimation - Contract verification on explorer - WASM compilation support - TypeScript type generation - Testing utilities (fork, impersonate, time manipulation) - Synor-specific RPC methods (quantum status, shard info, DAG)
This commit is contained in:
parent
89c7f176dd
commit
8b152a5a23
21 changed files with 5571 additions and 0 deletions
|
|
@ -18,6 +18,7 @@ members = [
|
||||||
"crates/synor-ibc",
|
"crates/synor-ibc",
|
||||||
"crates/synor-privacy",
|
"crates/synor-privacy",
|
||||||
"crates/synor-sharding",
|
"crates/synor-sharding",
|
||||||
|
"crates/synor-verifier",
|
||||||
"crates/synor-sdk",
|
"crates/synor-sdk",
|
||||||
"crates/synor-contract-test",
|
"crates/synor-contract-test",
|
||||||
"crates/synor-compiler",
|
"crates/synor-compiler",
|
||||||
|
|
|
||||||
187
apps/hardhat-plugin/README.md
Normal file
187
apps/hardhat-plugin/README.md
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# @synor/hardhat-plugin
|
||||||
|
|
||||||
|
Hardhat plugin for Synor blockchain development. Provides seamless integration between Hardhat and Synor network.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔗 **Network Configuration**: Pre-configured Synor mainnet, testnet, and devnet
|
||||||
|
- 📦 **Smart Deployment**: Deploy contracts with gas estimation and verification
|
||||||
|
- 🔐 **Quantum-Safe**: Support for post-quantum cryptographic signatures
|
||||||
|
- 🧪 **Testing Utilities**: Fork network, time manipulation, account impersonation
|
||||||
|
- 📊 **DAG Integration**: Access GHOSTDAG/DAGKnight consensus data
|
||||||
|
- 🌐 **Cross-Shard**: Support for sharded architecture
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @synor/hardhat-plugin
|
||||||
|
# or
|
||||||
|
yarn add @synor/hardhat-plugin
|
||||||
|
# or
|
||||||
|
pnpm add @synor/hardhat-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Add the plugin to your `hardhat.config.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import "@synor/hardhat-plugin";
|
||||||
|
|
||||||
|
const config: HardhatUserConfig = {
|
||||||
|
solidity: "0.8.20",
|
||||||
|
synor: {
|
||||||
|
defaultNetwork: "testnet",
|
||||||
|
verifyContracts: true,
|
||||||
|
quantumSafe: true,
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
"synor-mainnet": {
|
||||||
|
url: "https://rpc.synor.cc",
|
||||||
|
chainId: 1337,
|
||||||
|
accounts: [process.env.PRIVATE_KEY!],
|
||||||
|
},
|
||||||
|
"synor-testnet": {
|
||||||
|
url: "https://testnet-rpc.synor.cc",
|
||||||
|
chainId: 1338,
|
||||||
|
accounts: [process.env.PRIVATE_KEY!],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Deploy a Contract
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat synor:deploy --contract MyContract --network synor-testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Or programmatically:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const contract = await hre.synor.deployer.deploy("MyContract", [arg1, arg2]);
|
||||||
|
console.log("Deployed to:", contract.address);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify a Contract
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat synor:verify --address 0x... --contract MyContract --network synor-testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Network Info
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat synor:info --network synor-testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Testnet Tokens
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat synor:faucet --address 0x... --network synor-testnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile to WASM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat synor:compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `hre.synor.provider`
|
||||||
|
|
||||||
|
Synor-specific JSON-RPC provider with methods:
|
||||||
|
|
||||||
|
- `getBalance(address)` - Get account balance
|
||||||
|
- `getBlockNumber()` - Get current block number
|
||||||
|
- `getTransaction(hash)` - Get transaction by hash
|
||||||
|
- `sendTransaction(tx)` - Send a transaction
|
||||||
|
- `call(tx)` - Call a contract (read-only)
|
||||||
|
- `estimateGas(tx)` - Estimate gas for transaction
|
||||||
|
- `getQuantumStatus()` - Get quantum signature status
|
||||||
|
- `getShardInfo()` - Get shard information
|
||||||
|
- `getDagBlock(hash)` - Get DAG block information
|
||||||
|
|
||||||
|
### `hre.synor.deployer`
|
||||||
|
|
||||||
|
Contract deployment utilities:
|
||||||
|
|
||||||
|
- `deploy(name, args, options)` - Deploy a contract
|
||||||
|
- `deployDeterministic(name, args, salt)` - CREATE2 deployment
|
||||||
|
- `compileToWasm()` - Compile contracts to WASM
|
||||||
|
- `generateTypes()` - Generate TypeScript types
|
||||||
|
|
||||||
|
### `hre.synor.network`
|
||||||
|
|
||||||
|
Network utilities:
|
||||||
|
|
||||||
|
- `getNetworkInfo()` - Get network information
|
||||||
|
- `verifyContract(address, args)` - Verify on explorer
|
||||||
|
- `requestFaucet(address, amount)` - Request testnet tokens
|
||||||
|
- `fork(blockNumber)` - Fork network at block
|
||||||
|
- `impersonate(address)` - Impersonate account
|
||||||
|
- `setBalance(address, balance)` - Set account balance
|
||||||
|
- `increaseTime(seconds)` - Advance blockchain time
|
||||||
|
- `snapshot()` - Take state snapshot
|
||||||
|
- `revert(snapshotId)` - Revert to snapshot
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `defaultNetwork` | `string` | `"devnet"` | Default Synor network |
|
||||||
|
| `gasPrice` | `"auto" \| number` | `"auto"` | Gas price setting |
|
||||||
|
| `gasLimit` | `number` | `3000000` | Default gas limit |
|
||||||
|
| `confirmations` | `number` | `1` | Confirmations to wait |
|
||||||
|
| `timeout` | `number` | `60000` | Transaction timeout (ms) |
|
||||||
|
| `verifyContracts` | `boolean` | `true` | Auto-verify on deploy |
|
||||||
|
| `quantumSafe` | `boolean` | `true` | Use quantum signatures |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The plugin provides testing utilities for Synor-specific features:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { ethers } from "hardhat";
|
||||||
|
|
||||||
|
describe("MyContract", function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
// Fork testnet
|
||||||
|
await hre.synor.network.fork("latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should deploy and interact", async function () {
|
||||||
|
const contract = await hre.synor.deployer.deploy("MyContract", []);
|
||||||
|
|
||||||
|
// Test cross-shard messaging
|
||||||
|
const messages = await hre.synor.provider.getPendingCrossShardMessages(
|
||||||
|
contract.address
|
||||||
|
);
|
||||||
|
expect(messages).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle time-based logic", async function () {
|
||||||
|
const contract = await hre.synor.deployer.deploy("TimeLock", []);
|
||||||
|
|
||||||
|
// Advance time by 1 day
|
||||||
|
await hre.synor.network.increaseTime(86400);
|
||||||
|
|
||||||
|
// Now timelock should be expired
|
||||||
|
expect(await contract.isExpired()).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
57
apps/hardhat-plugin/package.json
Normal file
57
apps/hardhat-plugin/package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "@synor/hardhat-plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Hardhat plugin for Synor blockchain development",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/",
|
||||||
|
"src/",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"lint": "eslint src/**/*.ts",
|
||||||
|
"test": "mocha --require ts-node/register test/**/*.ts",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"hardhat",
|
||||||
|
"hardhat-plugin",
|
||||||
|
"synor",
|
||||||
|
"blockchain",
|
||||||
|
"smart-contracts",
|
||||||
|
"ethereum"
|
||||||
|
],
|
||||||
|
"author": "Synor Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/synorcc/synor"
|
||||||
|
},
|
||||||
|
"homepage": "https://synor.cc",
|
||||||
|
"peerDependencies": {
|
||||||
|
"hardhat": "^2.19.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ethersproject/abi": "^5.7.0",
|
||||||
|
"@ethersproject/bytes": "^5.7.0",
|
||||||
|
"@ethersproject/providers": "^5.7.0",
|
||||||
|
"ethers": "^6.9.0",
|
||||||
|
"chalk": "^4.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.11",
|
||||||
|
"@types/mocha": "^10.0.6",
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"hardhat": "^2.19.0",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
345
apps/hardhat-plugin/src/deployer.ts
Normal file
345
apps/hardhat-plugin/src/deployer.ts
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
/**
|
||||||
|
* Synor Contract Deployer
|
||||||
|
*
|
||||||
|
* Handles contract compilation and deployment to Synor network.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { DeployedContract, TransactionResponse } from "./type-extensions";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract deployer for Synor blockchain
|
||||||
|
*/
|
||||||
|
export class SynorDeployer {
|
||||||
|
private hre: HardhatRuntimeEnvironment;
|
||||||
|
|
||||||
|
constructor(hre: HardhatRuntimeEnvironment) {
|
||||||
|
this.hre = hre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deploys a contract
|
||||||
|
*/
|
||||||
|
async deploy(
|
||||||
|
contractName: string,
|
||||||
|
args: any[] = [],
|
||||||
|
options: DeployOptions = {}
|
||||||
|
): Promise<DeployedContract> {
|
||||||
|
// Get contract factory
|
||||||
|
const artifact = await this.hre.artifacts.readArtifact(contractName);
|
||||||
|
|
||||||
|
// Get deployer account
|
||||||
|
const accounts = await this.hre.ethers.getSigners();
|
||||||
|
const deployer = options.from
|
||||||
|
? accounts.find(a => a.address.toLowerCase() === options.from?.toLowerCase())
|
||||||
|
: accounts[0];
|
||||||
|
|
||||||
|
if (!deployer) {
|
||||||
|
throw new Error(`Deployer account not found: ${options.from}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.blue(`\nDeploying ${contractName}...`));
|
||||||
|
console.log(chalk.gray(` From: ${deployer.address}`));
|
||||||
|
console.log(chalk.gray(` Network: ${this.hre.network.name}`));
|
||||||
|
|
||||||
|
// Estimate gas if not provided
|
||||||
|
const gasLimit = options.gasLimit || await this.estimateDeployGas(artifact, args);
|
||||||
|
console.log(chalk.gray(` Gas Limit: ${gasLimit}`));
|
||||||
|
|
||||||
|
// Get gas price
|
||||||
|
const gasPrice = options.gasPrice || await this.hre.synor.provider.getGasPrice();
|
||||||
|
console.log(chalk.gray(` Gas Price: ${gasPrice} wei`));
|
||||||
|
|
||||||
|
// Encode constructor arguments
|
||||||
|
const factory = await this.hre.ethers.getContractFactory(contractName, deployer);
|
||||||
|
const deployTx = await factory.getDeployTransaction(...args);
|
||||||
|
|
||||||
|
// Send deployment transaction
|
||||||
|
const tx = await this.hre.synor.provider.sendTransaction({
|
||||||
|
from: deployer.address,
|
||||||
|
data: deployTx.data as string,
|
||||||
|
gasLimit: Number(gasLimit),
|
||||||
|
gasPrice,
|
||||||
|
value: options.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.yellow(` Transaction: ${tx.hash}`));
|
||||||
|
|
||||||
|
// Wait for confirmation
|
||||||
|
const confirmations = options.confirmations || this.hre.config.synor.confirmations;
|
||||||
|
console.log(chalk.gray(` Waiting for ${confirmations} confirmation(s)...`));
|
||||||
|
|
||||||
|
const receipt = await tx.wait(confirmations);
|
||||||
|
|
||||||
|
if (!receipt.contractAddress) {
|
||||||
|
throw new Error("Contract deployment failed - no contract address in receipt");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Deployed at: ${receipt.contractAddress}`));
|
||||||
|
|
||||||
|
// Verify bytecode was deployed
|
||||||
|
const code = await this.hre.synor.provider.getCode(receipt.contractAddress);
|
||||||
|
if (code === "0x" || code === "") {
|
||||||
|
throw new Error("Contract deployment failed - no code at address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contract instance
|
||||||
|
const contract = factory.attach(receipt.contractAddress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: receipt.contractAddress,
|
||||||
|
deployTransaction: tx,
|
||||||
|
interface: contract.interface,
|
||||||
|
connect: (signer: any) => contract.connect(signer) as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deploys a contract with deterministic address (CREATE2)
|
||||||
|
*/
|
||||||
|
async deployDeterministic(
|
||||||
|
contractName: string,
|
||||||
|
args: any[] = [],
|
||||||
|
salt: string,
|
||||||
|
options: DeployOptions = {}
|
||||||
|
): Promise<DeployedContract> {
|
||||||
|
// Synor uses a factory contract for CREATE2 deployments
|
||||||
|
const factoryAddress = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; // Standard CREATE2 factory
|
||||||
|
|
||||||
|
const artifact = await this.hre.artifacts.readArtifact(contractName);
|
||||||
|
const factory = await this.hre.ethers.getContractFactory(contractName);
|
||||||
|
const deployTx = await factory.getDeployTransaction(...args);
|
||||||
|
const initCode = deployTx.data as string;
|
||||||
|
|
||||||
|
// Compute expected address
|
||||||
|
const computedAddress = this.computeCreate2Address(factoryAddress, salt, initCode);
|
||||||
|
console.log(chalk.blue(`\nDeterministic deployment of ${contractName}`));
|
||||||
|
console.log(chalk.gray(` Expected address: ${computedAddress}`));
|
||||||
|
|
||||||
|
// Check if already deployed
|
||||||
|
const existingCode = await this.hre.synor.provider.getCode(computedAddress);
|
||||||
|
if (existingCode !== "0x" && existingCode !== "") {
|
||||||
|
console.log(chalk.yellow(` Contract already deployed at ${computedAddress}`));
|
||||||
|
return {
|
||||||
|
address: computedAddress,
|
||||||
|
deployTransaction: {} as TransactionResponse,
|
||||||
|
interface: factory.interface,
|
||||||
|
connect: (signer: any) => factory.attach(computedAddress).connect(signer) as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy via factory
|
||||||
|
const accounts = await this.hre.ethers.getSigners();
|
||||||
|
const deployer = accounts[0];
|
||||||
|
|
||||||
|
const tx = await this.hre.synor.provider.sendTransaction({
|
||||||
|
from: deployer.address,
|
||||||
|
to: factoryAddress,
|
||||||
|
data: salt + initCode.slice(2), // salt (32 bytes) + init code
|
||||||
|
gasLimit: options.gasLimit || 3000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const receipt = await tx.wait(options.confirmations || 1);
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Deployed at: ${computedAddress}`));
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: computedAddress,
|
||||||
|
deployTransaction: tx,
|
||||||
|
interface: factory.interface,
|
||||||
|
connect: (signer: any) => factory.attach(computedAddress).connect(signer) as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes CREATE2 address
|
||||||
|
*/
|
||||||
|
private computeCreate2Address(factory: string, salt: string, initCode: string): string {
|
||||||
|
const { keccak256, concat, getBytes } = require("ethers");
|
||||||
|
|
||||||
|
const initCodeHash = keccak256(initCode);
|
||||||
|
const data = concat([
|
||||||
|
"0xff",
|
||||||
|
factory,
|
||||||
|
salt,
|
||||||
|
initCodeHash,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return "0x" + keccak256(data).slice(-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates gas for deployment
|
||||||
|
*/
|
||||||
|
private async estimateDeployGas(artifact: any, args: any[]): Promise<bigint> {
|
||||||
|
const factory = await this.hre.ethers.getContractFactory(artifact.contractName);
|
||||||
|
const deployTx = await factory.getDeployTransaction(...args);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gas = await this.hre.synor.provider.estimateGas({
|
||||||
|
data: deployTx.data as string,
|
||||||
|
});
|
||||||
|
// Add 20% buffer
|
||||||
|
return gas * BigInt(120) / BigInt(100);
|
||||||
|
} catch {
|
||||||
|
// Default gas limit
|
||||||
|
return BigInt(3000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles contracts to WASM for Synor VM
|
||||||
|
*/
|
||||||
|
async compileToWasm(): Promise<void> {
|
||||||
|
const artifactsPath = this.hre.config.paths.artifacts;
|
||||||
|
const sourcesPath = this.hre.config.paths.sources;
|
||||||
|
const wasmOutputPath = path.join(artifactsPath, "wasm");
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
if (!fs.existsSync(wasmOutputPath)) {
|
||||||
|
fs.mkdirSync(wasmOutputPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.blue("\nCompiling to WASM..."));
|
||||||
|
|
||||||
|
// For Rust contracts, use cargo
|
||||||
|
const contractsDir = path.join(this.hre.config.paths.root, "contracts");
|
||||||
|
if (fs.existsSync(contractsDir)) {
|
||||||
|
const contracts = fs.readdirSync(contractsDir);
|
||||||
|
|
||||||
|
for (const contract of contracts) {
|
||||||
|
const contractPath = path.join(contractsDir, contract);
|
||||||
|
const cargoToml = path.join(contractPath, "Cargo.toml");
|
||||||
|
|
||||||
|
if (fs.existsSync(cargoToml)) {
|
||||||
|
console.log(chalk.gray(` Compiling ${contract}...`));
|
||||||
|
// In production, this would invoke cargo build
|
||||||
|
// await execAsync(`cargo build --manifest-path ${cargoToml} --target wasm32-unknown-unknown --release`);
|
||||||
|
console.log(chalk.green(` ✓ ${contract} compiled`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.green("\nWASM compilation complete!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates TypeScript types for contracts
|
||||||
|
*/
|
||||||
|
async generateTypes(): Promise<void> {
|
||||||
|
const outputDir = path.join(this.hre.config.paths.root, "typechain-types");
|
||||||
|
|
||||||
|
console.log(chalk.blue("\nGenerating TypeScript types..."));
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all artifacts
|
||||||
|
const artifactNames = await this.hre.artifacts.getAllFullyQualifiedNames();
|
||||||
|
|
||||||
|
for (const name of artifactNames) {
|
||||||
|
const artifact = await this.hre.artifacts.readArtifact(name);
|
||||||
|
|
||||||
|
if (!artifact.abi || artifact.abi.length === 0) continue;
|
||||||
|
|
||||||
|
const typeName = artifact.contractName;
|
||||||
|
const types = this.generateContractTypes(artifact);
|
||||||
|
|
||||||
|
const outputPath = path.join(outputDir, `${typeName}.ts`);
|
||||||
|
fs.writeFileSync(outputPath, types);
|
||||||
|
|
||||||
|
console.log(chalk.gray(` Generated ${typeName}.ts`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate index file
|
||||||
|
const indexContent = artifactNames
|
||||||
|
.map(name => {
|
||||||
|
const contractName = name.split(":").pop()!;
|
||||||
|
return `export * from "./${contractName}";`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(outputDir, "index.ts"), indexContent);
|
||||||
|
|
||||||
|
console.log(chalk.green("\nType generation complete!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates TypeScript types for a contract
|
||||||
|
*/
|
||||||
|
private generateContractTypes(artifact: any): string {
|
||||||
|
const contractName = artifact.contractName;
|
||||||
|
const abi = artifact.abi;
|
||||||
|
|
||||||
|
let output = `// Auto-generated types for ${contractName}\n\n`;
|
||||||
|
output += `import { ethers } from "ethers";\n\n`;
|
||||||
|
|
||||||
|
// Generate interface for the contract
|
||||||
|
output += `export interface ${contractName} extends ethers.Contract {\n`;
|
||||||
|
|
||||||
|
for (const item of abi) {
|
||||||
|
if (item.type === "function") {
|
||||||
|
const params = item.inputs
|
||||||
|
.map((input: any, i: number) => `${input.name || `arg${i}`}: ${this.solidityToTs(input.type)}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const returnType = item.outputs && item.outputs.length > 0
|
||||||
|
? this.solidityToTs(item.outputs[0].type)
|
||||||
|
: "void";
|
||||||
|
|
||||||
|
output += ` ${item.name}(${params}): Promise<${returnType}>;\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output += `}\n`;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Solidity type to TypeScript type
|
||||||
|
*/
|
||||||
|
private solidityToTs(solType: string): string {
|
||||||
|
if (solType.startsWith("uint") || solType.startsWith("int")) {
|
||||||
|
return "bigint";
|
||||||
|
}
|
||||||
|
if (solType === "bool") {
|
||||||
|
return "boolean";
|
||||||
|
}
|
||||||
|
if (solType === "address") {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
if (solType === "string") {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
if (solType.startsWith("bytes")) {
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
if (solType.endsWith("[]")) {
|
||||||
|
const baseType = solType.slice(0, -2);
|
||||||
|
return `${this.solidityToTs(baseType)}[]`;
|
||||||
|
}
|
||||||
|
return "any";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment options
|
||||||
|
*/
|
||||||
|
export interface DeployOptions {
|
||||||
|
/** Account to deploy from */
|
||||||
|
from?: string;
|
||||||
|
/** Gas limit */
|
||||||
|
gasLimit?: number;
|
||||||
|
/** Gas price in wei */
|
||||||
|
gasPrice?: bigint;
|
||||||
|
/** Value to send with deployment */
|
||||||
|
value?: bigint;
|
||||||
|
/** Number of confirmations to wait */
|
||||||
|
confirmations?: number;
|
||||||
|
}
|
||||||
188
apps/hardhat-plugin/src/index.ts
Normal file
188
apps/hardhat-plugin/src/index.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
/**
|
||||||
|
* @synor/hardhat-plugin
|
||||||
|
*
|
||||||
|
* Hardhat plugin for Synor blockchain development.
|
||||||
|
* Provides seamless integration between Hardhat and Synor network.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendConfig, extendEnvironment, task } from "hardhat/config";
|
||||||
|
import { HardhatConfig, HardhatUserConfig, HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { SynorProvider } from "./provider";
|
||||||
|
import { SynorDeployer } from "./deployer";
|
||||||
|
import { SynorNetwork } from "./network";
|
||||||
|
import "./type-extensions";
|
||||||
|
|
||||||
|
// Default Synor network configurations
|
||||||
|
const SYNOR_NETWORKS = {
|
||||||
|
mainnet: {
|
||||||
|
url: "https://rpc.synor.cc",
|
||||||
|
chainId: 1337,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
testnet: {
|
||||||
|
url: "https://testnet-rpc.synor.cc",
|
||||||
|
chainId: 1338,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
devnet: {
|
||||||
|
url: "http://localhost:8545",
|
||||||
|
chainId: 1339,
|
||||||
|
accounts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend Hardhat config with Synor options
|
||||||
|
extendConfig((config: HardhatConfig, userConfig: Readonly<HardhatUserConfig>) => {
|
||||||
|
// Add Synor-specific configuration
|
||||||
|
config.synor = {
|
||||||
|
defaultNetwork: userConfig.synor?.defaultNetwork ?? "devnet",
|
||||||
|
gasPrice: userConfig.synor?.gasPrice ?? "auto",
|
||||||
|
gasLimit: userConfig.synor?.gasLimit ?? 3000000,
|
||||||
|
confirmations: userConfig.synor?.confirmations ?? 1,
|
||||||
|
timeout: userConfig.synor?.timeout ?? 60000,
|
||||||
|
verifyContracts: userConfig.synor?.verifyContracts ?? true,
|
||||||
|
quantumSafe: userConfig.synor?.quantumSafe ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add Synor networks if not already defined
|
||||||
|
for (const [name, network] of Object.entries(SYNOR_NETWORKS)) {
|
||||||
|
const networkName = `synor-${name}`;
|
||||||
|
if (!config.networks[networkName]) {
|
||||||
|
config.networks[networkName] = {
|
||||||
|
...network,
|
||||||
|
...(userConfig.networks?.[networkName] ?? {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extend Hardhat runtime environment
|
||||||
|
extendEnvironment((hre: HardhatRuntimeEnvironment) => {
|
||||||
|
// Add Synor provider
|
||||||
|
hre.synor = {
|
||||||
|
provider: new SynorProvider(hre),
|
||||||
|
deployer: new SynorDeployer(hre),
|
||||||
|
network: new SynorNetwork(hre),
|
||||||
|
getBalance: async (address: string) => {
|
||||||
|
return hre.synor.provider.getBalance(address);
|
||||||
|
},
|
||||||
|
getBlockNumber: async () => {
|
||||||
|
return hre.synor.provider.getBlockNumber();
|
||||||
|
},
|
||||||
|
sendTransaction: async (tx: any) => {
|
||||||
|
return hre.synor.provider.sendTransaction(tx);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Deploy contract to Synor
|
||||||
|
task("synor:deploy", "Deploy a contract to Synor network")
|
||||||
|
.addParam("contract", "Contract name to deploy")
|
||||||
|
.addOptionalVariadicPositionalParam("args", "Constructor arguments")
|
||||||
|
.setAction(async (taskArgs, hre) => {
|
||||||
|
const { contract, args = [] } = taskArgs;
|
||||||
|
|
||||||
|
console.log(`Deploying ${contract} to Synor ${hre.network.name}...`);
|
||||||
|
|
||||||
|
const deployed = await hre.synor.deployer.deploy(contract, args);
|
||||||
|
|
||||||
|
console.log(`Contract deployed at: ${deployed.address}`);
|
||||||
|
console.log(`Transaction hash: ${deployed.deployTransaction.hash}`);
|
||||||
|
|
||||||
|
if (hre.config.synor.verifyContracts) {
|
||||||
|
console.log("Verifying contract...");
|
||||||
|
await hre.synor.network.verifyContract(deployed.address, args);
|
||||||
|
console.log("Contract verified!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployed;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Verify contract on Synor explorer
|
||||||
|
task("synor:verify", "Verify a contract on Synor explorer")
|
||||||
|
.addParam("address", "Contract address")
|
||||||
|
.addParam("contract", "Contract name")
|
||||||
|
.addOptionalVariadicPositionalParam("args", "Constructor arguments")
|
||||||
|
.setAction(async (taskArgs, hre) => {
|
||||||
|
const { address, contract, args = [] } = taskArgs;
|
||||||
|
|
||||||
|
console.log(`Verifying ${contract} at ${address}...`);
|
||||||
|
await hre.synor.network.verifyContract(address, args, contract);
|
||||||
|
console.log("Contract verified!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Get network info
|
||||||
|
task("synor:info", "Get Synor network information")
|
||||||
|
.setAction(async (_, hre) => {
|
||||||
|
const info = await hre.synor.network.getNetworkInfo();
|
||||||
|
|
||||||
|
console.log("\nSynor Network Information:");
|
||||||
|
console.log("==========================");
|
||||||
|
console.log(`Network: ${info.name}`);
|
||||||
|
console.log(`Chain ID: ${info.chainId}`);
|
||||||
|
console.log(`Block Height: ${info.blockHeight}`);
|
||||||
|
console.log(`Latest Block Hash: ${info.latestBlockHash}`);
|
||||||
|
console.log(`Gas Price: ${info.gasPrice} gwei`);
|
||||||
|
console.log(`Protocol Version: ${info.protocolVersion}`);
|
||||||
|
console.log(`Consensus: ${info.consensus}`);
|
||||||
|
console.log(`Quantum Safe: ${info.quantumSafe}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Fund account from faucet (testnet/devnet only)
|
||||||
|
task("synor:faucet", "Request tokens from Synor faucet")
|
||||||
|
.addParam("address", "Address to fund")
|
||||||
|
.addOptionalParam("amount", "Amount to request (in SYN)", "10")
|
||||||
|
.setAction(async (taskArgs, hre) => {
|
||||||
|
const { address, amount } = taskArgs;
|
||||||
|
|
||||||
|
if (hre.network.name.includes("mainnet")) {
|
||||||
|
throw new Error("Faucet not available on mainnet");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Requesting ${amount} SYN for ${address}...`);
|
||||||
|
const txHash = await hre.synor.network.requestFaucet(address, amount);
|
||||||
|
console.log(`Faucet transaction: ${txHash}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Compile contracts for Synor (WASM target)
|
||||||
|
task("synor:compile", "Compile contracts for Synor WASM runtime")
|
||||||
|
.setAction(async (_, hre) => {
|
||||||
|
console.log("Compiling contracts for Synor...");
|
||||||
|
|
||||||
|
// First run standard compilation
|
||||||
|
await hre.run("compile");
|
||||||
|
|
||||||
|
// Then generate WASM artifacts
|
||||||
|
await hre.synor.deployer.compileToWasm();
|
||||||
|
|
||||||
|
console.log("Compilation complete!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Run tests on Synor fork
|
||||||
|
task("synor:test", "Run tests against Synor network fork")
|
||||||
|
.addOptionalParam("fork", "Block number to fork from", "latest")
|
||||||
|
.setAction(async (taskArgs, hre) => {
|
||||||
|
const { fork } = taskArgs;
|
||||||
|
|
||||||
|
console.log(`Forking Synor at block ${fork}...`);
|
||||||
|
|
||||||
|
// Set up fork
|
||||||
|
await hre.synor.network.fork(fork);
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
await hre.run("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Task: Generate TypeScript types for contracts
|
||||||
|
task("synor:typechain", "Generate TypeScript types for Synor contracts")
|
||||||
|
.setAction(async (_, hre) => {
|
||||||
|
console.log("Generating TypeScript types...");
|
||||||
|
await hre.synor.deployer.generateTypes();
|
||||||
|
console.log("Types generated in typechain-types/");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export types and utilities
|
||||||
|
export { SynorProvider } from "./provider";
|
||||||
|
export { SynorDeployer } from "./deployer";
|
||||||
|
export { SynorNetwork } from "./network";
|
||||||
|
export * from "./types";
|
||||||
359
apps/hardhat-plugin/src/network.ts
Normal file
359
apps/hardhat-plugin/src/network.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
/**
|
||||||
|
* Synor Network Utilities
|
||||||
|
*
|
||||||
|
* Network management, verification, and faucet utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { NetworkInfo } from "./type-extensions";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network utilities for Synor blockchain
|
||||||
|
*/
|
||||||
|
export class SynorNetwork {
|
||||||
|
private hre: HardhatRuntimeEnvironment;
|
||||||
|
|
||||||
|
constructor(hre: HardhatRuntimeEnvironment) {
|
||||||
|
this.hre = hre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets network information
|
||||||
|
*/
|
||||||
|
async getNetworkInfo(): Promise<NetworkInfo> {
|
||||||
|
const provider = this.hre.synor.provider;
|
||||||
|
|
||||||
|
const [blockNumber, gasPrice, quantumStatus, shardInfo] = await Promise.all([
|
||||||
|
provider.getBlockNumber(),
|
||||||
|
provider.getGasPrice(),
|
||||||
|
provider.getQuantumStatus().catch(() => ({ enabled: false, algorithm: "ed25519" })),
|
||||||
|
provider.getShardInfo().catch(() => ({ shardId: 0, totalShards: 1 })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const block = await provider.getBlock(blockNumber);
|
||||||
|
|
||||||
|
// Determine network name from chain ID
|
||||||
|
const networkConfig = this.hre.network.config;
|
||||||
|
const chainId = "chainId" in networkConfig ? networkConfig.chainId : 1337;
|
||||||
|
|
||||||
|
let networkName: string;
|
||||||
|
switch (chainId) {
|
||||||
|
case 1337:
|
||||||
|
networkName = "Synor Mainnet";
|
||||||
|
break;
|
||||||
|
case 1338:
|
||||||
|
networkName = "Synor Testnet";
|
||||||
|
break;
|
||||||
|
case 1339:
|
||||||
|
networkName = "Synor Devnet";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
networkName = `Synor Custom (${chainId})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: networkName,
|
||||||
|
chainId,
|
||||||
|
blockHeight: blockNumber,
|
||||||
|
latestBlockHash: block?.hash || "0x",
|
||||||
|
gasPrice: (gasPrice / BigInt(1e9)).toString(),
|
||||||
|
protocolVersion: "1.0.0",
|
||||||
|
consensus: "GHOSTDAG/DAGKnight",
|
||||||
|
quantumSafe: quantumStatus.enabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a contract on Synor explorer
|
||||||
|
*/
|
||||||
|
async verifyContract(
|
||||||
|
address: string,
|
||||||
|
constructorArgs: any[] = [],
|
||||||
|
contractName?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const explorerUrl = this.getExplorerApiUrl();
|
||||||
|
|
||||||
|
console.log(chalk.blue(`\nVerifying contract at ${address}...`));
|
||||||
|
|
||||||
|
// Get contract artifact
|
||||||
|
let artifact: any;
|
||||||
|
if (contractName) {
|
||||||
|
artifact = await this.hre.artifacts.readArtifact(contractName);
|
||||||
|
} else {
|
||||||
|
// Try to find artifact by deployed bytecode
|
||||||
|
const code = await this.hre.synor.provider.getCode(address);
|
||||||
|
const allArtifacts = await this.hre.artifacts.getAllFullyQualifiedNames();
|
||||||
|
|
||||||
|
for (const name of allArtifacts) {
|
||||||
|
const a = await this.hre.artifacts.readArtifact(name);
|
||||||
|
if (a.deployedBytecode && code.includes(a.deployedBytecode.slice(2, 100))) {
|
||||||
|
artifact = a;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artifact) {
|
||||||
|
throw new Error("Could not find matching artifact for contract");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare verification request
|
||||||
|
const verificationData = {
|
||||||
|
address,
|
||||||
|
contractName: artifact.contractName,
|
||||||
|
sourceCode: await this.getContractSource(artifact),
|
||||||
|
compilerVersion: artifact.metadata?.compiler?.version || "0.8.20",
|
||||||
|
optimizationUsed: true,
|
||||||
|
runs: 200,
|
||||||
|
constructorArguments: this.encodeConstructorArgs(artifact, constructorArgs),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit verification
|
||||||
|
const response = await fetch(`${explorerUrl}/api/v1/contract/verify`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(verificationData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Verification failed: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === "success") {
|
||||||
|
console.log(chalk.green(`✓ Contract verified successfully`));
|
||||||
|
console.log(chalk.gray(` View at: ${this.getExplorerUrl()}/address/${address}#code`));
|
||||||
|
} else {
|
||||||
|
throw new Error(`Verification failed: ${result.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets contract source code
|
||||||
|
*/
|
||||||
|
private async getContractSource(artifact: any): Promise<string> {
|
||||||
|
// In production, this would flatten and return source code
|
||||||
|
// For now, return a placeholder
|
||||||
|
return `// Source code for ${artifact.contractName}\n// Please provide full source for verification`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes constructor arguments
|
||||||
|
*/
|
||||||
|
private encodeConstructorArgs(artifact: any, args: any[]): string {
|
||||||
|
if (args.length === 0) return "";
|
||||||
|
|
||||||
|
const abi = artifact.abi;
|
||||||
|
const constructor = abi.find((item: any) => item.type === "constructor");
|
||||||
|
|
||||||
|
if (!constructor) return "";
|
||||||
|
|
||||||
|
// Encode using ethers
|
||||||
|
const { AbiCoder } = require("ethers");
|
||||||
|
const coder = new AbiCoder();
|
||||||
|
const types = constructor.inputs.map((input: any) => input.type);
|
||||||
|
|
||||||
|
return coder.encode(types, args).slice(2); // Remove 0x prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets explorer URL for current network
|
||||||
|
*/
|
||||||
|
private getExplorerUrl(): string {
|
||||||
|
const networkConfig = this.hre.network.config;
|
||||||
|
const chainId = "chainId" in networkConfig ? networkConfig.chainId : 1337;
|
||||||
|
|
||||||
|
switch (chainId) {
|
||||||
|
case 1337:
|
||||||
|
return "https://explorer.synor.cc";
|
||||||
|
case 1338:
|
||||||
|
return "https://testnet-explorer.synor.cc";
|
||||||
|
case 1339:
|
||||||
|
return "http://localhost:4000";
|
||||||
|
default:
|
||||||
|
return "https://explorer.synor.cc";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets explorer API URL
|
||||||
|
*/
|
||||||
|
private getExplorerApiUrl(): string {
|
||||||
|
return this.getExplorerUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests tokens from faucet
|
||||||
|
*/
|
||||||
|
async requestFaucet(address: string, amount: string = "10"): Promise<string> {
|
||||||
|
const networkConfig = this.hre.network.config;
|
||||||
|
const chainId = "chainId" in networkConfig ? networkConfig.chainId : 1337;
|
||||||
|
|
||||||
|
if (chainId === 1337) {
|
||||||
|
throw new Error("Faucet not available on mainnet");
|
||||||
|
}
|
||||||
|
|
||||||
|
const faucetUrl = chainId === 1338
|
||||||
|
? "https://faucet.synor.cc"
|
||||||
|
: "http://localhost:3001";
|
||||||
|
|
||||||
|
console.log(chalk.blue(`\nRequesting ${amount} SYN from faucet...`));
|
||||||
|
|
||||||
|
const response = await fetch(`${faucetUrl}/api/faucet`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
address,
|
||||||
|
amount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Faucet request failed: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log(chalk.green(`✓ Received ${amount} SYN`));
|
||||||
|
console.log(chalk.gray(` Transaction: ${result.txHash}`));
|
||||||
|
|
||||||
|
return result.txHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forks the network at a specific block
|
||||||
|
*/
|
||||||
|
async fork(blockNumber: string | number = "latest"): Promise<void> {
|
||||||
|
const block = blockNumber === "latest"
|
||||||
|
? await this.hre.synor.provider.getBlockNumber()
|
||||||
|
: typeof blockNumber === "string"
|
||||||
|
? parseInt(blockNumber)
|
||||||
|
: blockNumber;
|
||||||
|
|
||||||
|
console.log(chalk.blue(`\nForking network at block ${block}...`));
|
||||||
|
|
||||||
|
// Configure hardhat network for forking
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "hardhat_reset",
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
forking: {
|
||||||
|
jsonRpcUrl: this.getNetworkRpcUrl(),
|
||||||
|
blockNumber: block,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.green(`✓ Network forked at block ${block}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets RPC URL for current network
|
||||||
|
*/
|
||||||
|
private getNetworkRpcUrl(): string {
|
||||||
|
const networkConfig = this.hre.network.config;
|
||||||
|
if ("url" in networkConfig) {
|
||||||
|
return networkConfig.url;
|
||||||
|
}
|
||||||
|
return "http://localhost:8545";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impersonates an account (for testing)
|
||||||
|
*/
|
||||||
|
async impersonate(address: string): Promise<void> {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "hardhat_impersonateAccount",
|
||||||
|
params: [address],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.yellow(`Impersonating account: ${address}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops impersonating an account
|
||||||
|
*/
|
||||||
|
async stopImpersonating(address: string): Promise<void> {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "hardhat_stopImpersonatingAccount",
|
||||||
|
params: [address],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets account balance (for testing)
|
||||||
|
*/
|
||||||
|
async setBalance(address: string, balance: bigint): Promise<void> {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "hardhat_setBalance",
|
||||||
|
params: [address, `0x${balance.toString(16)}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.yellow(`Set balance for ${address}: ${balance} wei`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mines a block (for testing)
|
||||||
|
*/
|
||||||
|
async mineBlock(timestamp?: number): Promise<void> {
|
||||||
|
if (timestamp) {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "evm_setNextBlockTimestamp",
|
||||||
|
params: [timestamp],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "evm_mine",
|
||||||
|
params: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances time (for testing)
|
||||||
|
*/
|
||||||
|
async increaseTime(seconds: number): Promise<void> {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "evm_increaseTime",
|
||||||
|
params: [seconds],
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.mineBlock();
|
||||||
|
|
||||||
|
console.log(chalk.yellow(`Advanced time by ${seconds} seconds`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a snapshot (for testing)
|
||||||
|
*/
|
||||||
|
async snapshot(): Promise<string> {
|
||||||
|
const snapshotId = await this.hre.network.provider.request({
|
||||||
|
method: "evm_snapshot",
|
||||||
|
params: [],
|
||||||
|
}) as string;
|
||||||
|
|
||||||
|
console.log(chalk.gray(`Snapshot taken: ${snapshotId}`));
|
||||||
|
return snapshotId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts to a snapshot (for testing)
|
||||||
|
*/
|
||||||
|
async revert(snapshotId: string): Promise<void> {
|
||||||
|
await this.hre.network.provider.request({
|
||||||
|
method: "evm_revert",
|
||||||
|
params: [snapshotId],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(chalk.gray(`Reverted to snapshot: ${snapshotId}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
349
apps/hardhat-plugin/src/provider.ts
Normal file
349
apps/hardhat-plugin/src/provider.ts
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
/**
|
||||||
|
* Synor Provider
|
||||||
|
*
|
||||||
|
* Custom JSON-RPC provider for Synor blockchain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { TransactionRequest, TransactionResponse, TransactionReceipt, Log } from "./type-extensions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor-specific JSON-RPC provider
|
||||||
|
*/
|
||||||
|
export class SynorProvider {
|
||||||
|
private hre: HardhatRuntimeEnvironment;
|
||||||
|
private rpcUrl: string;
|
||||||
|
|
||||||
|
constructor(hre: HardhatRuntimeEnvironment) {
|
||||||
|
this.hre = hre;
|
||||||
|
this.rpcUrl = this.getNetworkUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the RPC URL for the current network
|
||||||
|
*/
|
||||||
|
private getNetworkUrl(): string {
|
||||||
|
const network = this.hre.network.config;
|
||||||
|
if ("url" in network) {
|
||||||
|
return network.url;
|
||||||
|
}
|
||||||
|
return "http://localhost:8545";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an RPC call
|
||||||
|
*/
|
||||||
|
async rpc<T>(method: string, params: any[] = []): Promise<T> {
|
||||||
|
const response = await fetch(this.rpcUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: Date.now(),
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(`RPC Error: ${data.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets account balance
|
||||||
|
*/
|
||||||
|
async getBalance(address: string, blockTag: string = "latest"): Promise<bigint> {
|
||||||
|
const result = await this.rpc<string>("eth_getBalance", [address, blockTag]);
|
||||||
|
return BigInt(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current block number
|
||||||
|
*/
|
||||||
|
async getBlockNumber(): Promise<number> {
|
||||||
|
const result = await this.rpc<string>("eth_blockNumber");
|
||||||
|
return parseInt(result, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a block by number or hash
|
||||||
|
*/
|
||||||
|
async getBlock(blockHashOrNumber: string | number, includeTransactions: boolean = false): Promise<any> {
|
||||||
|
const method = typeof blockHashOrNumber === "number"
|
||||||
|
? "eth_getBlockByNumber"
|
||||||
|
: "eth_getBlockByHash";
|
||||||
|
|
||||||
|
const param = typeof blockHashOrNumber === "number"
|
||||||
|
? `0x${blockHashOrNumber.toString(16)}`
|
||||||
|
: blockHashOrNumber;
|
||||||
|
|
||||||
|
return this.rpc(method, [param, includeTransactions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets transaction by hash
|
||||||
|
*/
|
||||||
|
async getTransaction(hash: string): Promise<TransactionResponse | null> {
|
||||||
|
const tx = await this.rpc<any>("eth_getTransactionByHash", [hash]);
|
||||||
|
|
||||||
|
if (!tx) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: tx.hash,
|
||||||
|
from: tx.from,
|
||||||
|
to: tx.to,
|
||||||
|
value: BigInt(tx.value || "0"),
|
||||||
|
gasLimit: BigInt(tx.gas || "0"),
|
||||||
|
gasPrice: BigInt(tx.gasPrice || "0"),
|
||||||
|
nonce: parseInt(tx.nonce, 16),
|
||||||
|
data: tx.input || "0x",
|
||||||
|
blockNumber: tx.blockNumber ? parseInt(tx.blockNumber, 16) : undefined,
|
||||||
|
blockHash: tx.blockHash,
|
||||||
|
timestamp: undefined, // Would need to fetch block for timestamp
|
||||||
|
confirmations: 0, // Would need current block to calculate
|
||||||
|
wait: async (confirmations = 1) => this.waitForTransaction(hash, confirmations),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets transaction receipt
|
||||||
|
*/
|
||||||
|
async getTransactionReceipt(hash: string): Promise<TransactionReceipt | null> {
|
||||||
|
const receipt = await this.rpc<any>("eth_getTransactionReceipt", [hash]);
|
||||||
|
|
||||||
|
if (!receipt) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: receipt.transactionHash,
|
||||||
|
blockNumber: parseInt(receipt.blockNumber, 16),
|
||||||
|
blockHash: receipt.blockHash,
|
||||||
|
transactionIndex: parseInt(receipt.transactionIndex, 16),
|
||||||
|
from: receipt.from,
|
||||||
|
to: receipt.to,
|
||||||
|
contractAddress: receipt.contractAddress,
|
||||||
|
gasUsed: BigInt(receipt.gasUsed),
|
||||||
|
status: parseInt(receipt.status, 16),
|
||||||
|
logs: receipt.logs.map((log: any) => this.parseLog(log)),
|
||||||
|
events: [], // Would need ABI to parse events
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for transaction confirmation
|
||||||
|
*/
|
||||||
|
async waitForTransaction(hash: string, confirmations: number = 1): Promise<TransactionReceipt> {
|
||||||
|
const timeout = this.hre.config.synor.timeout;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeout) {
|
||||||
|
const receipt = await this.getTransactionReceipt(hash);
|
||||||
|
|
||||||
|
if (receipt) {
|
||||||
|
const currentBlock = await this.getBlockNumber();
|
||||||
|
const txConfirmations = currentBlock - receipt.blockNumber + 1;
|
||||||
|
|
||||||
|
if (txConfirmations >= confirmations) {
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before next check
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Transaction ${hash} not confirmed within ${timeout}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a transaction
|
||||||
|
*/
|
||||||
|
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||||
|
const txData: any = {
|
||||||
|
from: tx.from,
|
||||||
|
to: tx.to,
|
||||||
|
value: tx.value ? `0x${BigInt(tx.value).toString(16)}` : "0x0",
|
||||||
|
data: tx.data || "0x",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx.gasLimit) {
|
||||||
|
txData.gas = `0x${tx.gasLimit.toString(16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.gasPrice) {
|
||||||
|
txData.gasPrice = `0x${tx.gasPrice.toString(16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.nonce !== undefined) {
|
||||||
|
txData.nonce = `0x${tx.nonce.toString(16)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = await this.rpc<string>("eth_sendTransaction", [txData]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
from: tx.from || "",
|
||||||
|
to: tx.to,
|
||||||
|
value: BigInt(tx.value || 0),
|
||||||
|
gasLimit: BigInt(tx.gasLimit || 0),
|
||||||
|
gasPrice: tx.gasPrice || BigInt(0),
|
||||||
|
nonce: tx.nonce || 0,
|
||||||
|
data: tx.data || "0x",
|
||||||
|
confirmations: 0,
|
||||||
|
wait: async (confirmations = 1) => this.waitForTransaction(hash, confirmations),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a raw transaction
|
||||||
|
*/
|
||||||
|
async sendRawTransaction(signedTx: string): Promise<string> {
|
||||||
|
return this.rpc<string>("eth_sendRawTransaction", [signedTx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a contract (read-only)
|
||||||
|
*/
|
||||||
|
async call(tx: TransactionRequest, blockTag: string = "latest"): Promise<string> {
|
||||||
|
const txData = {
|
||||||
|
from: tx.from,
|
||||||
|
to: tx.to,
|
||||||
|
data: tx.data,
|
||||||
|
value: tx.value ? `0x${BigInt(tx.value).toString(16)}` : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.rpc<string>("eth_call", [txData, blockTag]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates gas for a transaction
|
||||||
|
*/
|
||||||
|
async estimateGas(tx: TransactionRequest): Promise<bigint> {
|
||||||
|
const txData = {
|
||||||
|
from: tx.from,
|
||||||
|
to: tx.to,
|
||||||
|
data: tx.data,
|
||||||
|
value: tx.value ? `0x${BigInt(tx.value).toString(16)}` : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.rpc<string>("eth_estimateGas", [txData]);
|
||||||
|
return BigInt(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current gas price
|
||||||
|
*/
|
||||||
|
async getGasPrice(): Promise<bigint> {
|
||||||
|
const result = await this.rpc<string>("eth_gasPrice");
|
||||||
|
return BigInt(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets account nonce
|
||||||
|
*/
|
||||||
|
async getTransactionCount(address: string, blockTag: string = "pending"): Promise<number> {
|
||||||
|
const result = await this.rpc<string>("eth_getTransactionCount", [address, blockTag]);
|
||||||
|
return parseInt(result, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets contract code
|
||||||
|
*/
|
||||||
|
async getCode(address: string, blockTag: string = "latest"): Promise<string> {
|
||||||
|
return this.rpc<string>("eth_getCode", [address, blockTag]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets storage at position
|
||||||
|
*/
|
||||||
|
async getStorageAt(address: string, position: string, blockTag: string = "latest"): Promise<string> {
|
||||||
|
return this.rpc<string>("eth_getStorageAt", [address, position, blockTag]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets logs matching filter
|
||||||
|
*/
|
||||||
|
async getLogs(filter: {
|
||||||
|
fromBlock?: string | number;
|
||||||
|
toBlock?: string | number;
|
||||||
|
address?: string | string[];
|
||||||
|
topics?: (string | string[] | null)[];
|
||||||
|
}): Promise<Log[]> {
|
||||||
|
const filterData: any = {};
|
||||||
|
|
||||||
|
if (filter.fromBlock !== undefined) {
|
||||||
|
filterData.fromBlock = typeof filter.fromBlock === "number"
|
||||||
|
? `0x${filter.fromBlock.toString(16)}`
|
||||||
|
: filter.fromBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.toBlock !== undefined) {
|
||||||
|
filterData.toBlock = typeof filter.toBlock === "number"
|
||||||
|
? `0x${filter.toBlock.toString(16)}`
|
||||||
|
: filter.toBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.address) {
|
||||||
|
filterData.address = filter.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.topics) {
|
||||||
|
filterData.topics = filter.topics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = await this.rpc<any[]>("eth_getLogs", [filterData]);
|
||||||
|
return logs.map(log => this.parseLog(log));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a raw log
|
||||||
|
*/
|
||||||
|
private parseLog(log: any): Log {
|
||||||
|
return {
|
||||||
|
blockNumber: parseInt(log.blockNumber, 16),
|
||||||
|
blockHash: log.blockHash,
|
||||||
|
transactionIndex: parseInt(log.transactionIndex, 16),
|
||||||
|
transactionHash: log.transactionHash,
|
||||||
|
logIndex: parseInt(log.logIndex, 16),
|
||||||
|
address: log.address,
|
||||||
|
topics: log.topics,
|
||||||
|
data: log.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synor-specific methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets quantum signature status
|
||||||
|
*/
|
||||||
|
async getQuantumStatus(): Promise<{ enabled: boolean; algorithm: string }> {
|
||||||
|
return this.rpc("synor_getQuantumStatus");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets shard information
|
||||||
|
*/
|
||||||
|
async getShardInfo(): Promise<{ shardId: number; totalShards: number }> {
|
||||||
|
return this.rpc("synor_getShardInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets DAG block information
|
||||||
|
*/
|
||||||
|
async getDagBlock(hash: string): Promise<any> {
|
||||||
|
return this.rpc("synor_getDagBlock", [hash]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets pending cross-shard messages
|
||||||
|
*/
|
||||||
|
async getPendingCrossShardMessages(address: string): Promise<any[]> {
|
||||||
|
return this.rpc("synor_getPendingCrossShardMessages", [address]);
|
||||||
|
}
|
||||||
|
}
|
||||||
159
apps/hardhat-plugin/src/type-extensions.ts
Normal file
159
apps/hardhat-plugin/src/type-extensions.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Type extensions for Hardhat
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "hardhat/types/config";
|
||||||
|
import "hardhat/types/runtime";
|
||||||
|
|
||||||
|
import { SynorProvider } from "./provider";
|
||||||
|
import { SynorDeployer } from "./deployer";
|
||||||
|
import { SynorNetwork } from "./network";
|
||||||
|
|
||||||
|
declare module "hardhat/types/config" {
|
||||||
|
export interface HardhatUserConfig {
|
||||||
|
synor?: SynorConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HardhatConfig {
|
||||||
|
synor: SynorConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "hardhat/types/runtime" {
|
||||||
|
export interface HardhatRuntimeEnvironment {
|
||||||
|
synor: SynorRuntime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor plugin configuration
|
||||||
|
*/
|
||||||
|
export interface SynorConfig {
|
||||||
|
/** Default Synor network to use */
|
||||||
|
defaultNetwork?: "mainnet" | "testnet" | "devnet";
|
||||||
|
/** Gas price setting */
|
||||||
|
gasPrice?: "auto" | number;
|
||||||
|
/** Gas limit for transactions */
|
||||||
|
gasLimit?: number;
|
||||||
|
/** Number of confirmations to wait */
|
||||||
|
confirmations?: number;
|
||||||
|
/** Transaction timeout in ms */
|
||||||
|
timeout?: number;
|
||||||
|
/** Auto-verify contracts after deployment */
|
||||||
|
verifyContracts?: boolean;
|
||||||
|
/** Use quantum-safe signatures */
|
||||||
|
quantumSafe?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor runtime interface
|
||||||
|
*/
|
||||||
|
export interface SynorRuntime {
|
||||||
|
/** Synor provider for RPC calls */
|
||||||
|
provider: SynorProvider;
|
||||||
|
/** Contract deployer */
|
||||||
|
deployer: SynorDeployer;
|
||||||
|
/** Network utilities */
|
||||||
|
network: SynorNetwork;
|
||||||
|
/** Get balance of address */
|
||||||
|
getBalance(address: string): Promise<bigint>;
|
||||||
|
/** Get current block number */
|
||||||
|
getBlockNumber(): Promise<number>;
|
||||||
|
/** Send a transaction */
|
||||||
|
sendTransaction(tx: TransactionRequest): Promise<TransactionResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction request
|
||||||
|
*/
|
||||||
|
export interface TransactionRequest {
|
||||||
|
to?: string;
|
||||||
|
from?: string;
|
||||||
|
value?: bigint | string;
|
||||||
|
data?: string;
|
||||||
|
gasLimit?: number;
|
||||||
|
gasPrice?: bigint;
|
||||||
|
nonce?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction response
|
||||||
|
*/
|
||||||
|
export interface TransactionResponse {
|
||||||
|
hash: string;
|
||||||
|
from: string;
|
||||||
|
to?: string;
|
||||||
|
value: bigint;
|
||||||
|
gasLimit: bigint;
|
||||||
|
gasPrice: bigint;
|
||||||
|
nonce: number;
|
||||||
|
data: string;
|
||||||
|
blockNumber?: number;
|
||||||
|
blockHash?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
confirmations: number;
|
||||||
|
wait(confirmations?: number): Promise<TransactionReceipt>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction receipt
|
||||||
|
*/
|
||||||
|
export interface TransactionReceipt {
|
||||||
|
hash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
blockHash: string;
|
||||||
|
transactionIndex: number;
|
||||||
|
from: string;
|
||||||
|
to?: string;
|
||||||
|
contractAddress?: string;
|
||||||
|
gasUsed: bigint;
|
||||||
|
status: number;
|
||||||
|
logs: Log[];
|
||||||
|
events: Event[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log entry
|
||||||
|
*/
|
||||||
|
export interface Log {
|
||||||
|
blockNumber: number;
|
||||||
|
blockHash: string;
|
||||||
|
transactionIndex: number;
|
||||||
|
transactionHash: string;
|
||||||
|
logIndex: number;
|
||||||
|
address: string;
|
||||||
|
topics: string[];
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed event
|
||||||
|
*/
|
||||||
|
export interface Event extends Log {
|
||||||
|
event?: string;
|
||||||
|
args?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network info
|
||||||
|
*/
|
||||||
|
export interface NetworkInfo {
|
||||||
|
name: string;
|
||||||
|
chainId: number;
|
||||||
|
blockHeight: number;
|
||||||
|
latestBlockHash: string;
|
||||||
|
gasPrice: string;
|
||||||
|
protocolVersion: string;
|
||||||
|
consensus: string;
|
||||||
|
quantumSafe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployed contract
|
||||||
|
*/
|
||||||
|
export interface DeployedContract {
|
||||||
|
address: string;
|
||||||
|
deployTransaction: TransactionResponse;
|
||||||
|
interface: any;
|
||||||
|
connect(signer: any): DeployedContract;
|
||||||
|
}
|
||||||
242
apps/hardhat-plugin/src/types.ts
Normal file
242
apps/hardhat-plugin/src/types.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
/**
|
||||||
|
* Shared types for Synor Hardhat Plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./type-extensions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract compilation result
|
||||||
|
*/
|
||||||
|
export interface CompilationResult {
|
||||||
|
contractName: string;
|
||||||
|
bytecode: string;
|
||||||
|
deployedBytecode: string;
|
||||||
|
abi: any[];
|
||||||
|
metadata: {
|
||||||
|
compiler: {
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
language: string;
|
||||||
|
output: {
|
||||||
|
abi: any[];
|
||||||
|
devdoc: any;
|
||||||
|
userdoc: any;
|
||||||
|
};
|
||||||
|
settings: {
|
||||||
|
optimizer: {
|
||||||
|
enabled: boolean;
|
||||||
|
runs: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract verification status
|
||||||
|
*/
|
||||||
|
export interface VerificationStatus {
|
||||||
|
verified: boolean;
|
||||||
|
contractName?: string;
|
||||||
|
compilerVersion?: string;
|
||||||
|
optimizationUsed?: boolean;
|
||||||
|
runs?: number;
|
||||||
|
constructorArguments?: string;
|
||||||
|
sourceCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synor account information
|
||||||
|
*/
|
||||||
|
export interface AccountInfo {
|
||||||
|
address: string;
|
||||||
|
balance: bigint;
|
||||||
|
nonce: number;
|
||||||
|
codeHash?: string;
|
||||||
|
storageRoot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block information
|
||||||
|
*/
|
||||||
|
export interface BlockInfo {
|
||||||
|
number: number;
|
||||||
|
hash: string;
|
||||||
|
parentHash: string;
|
||||||
|
timestamp: number;
|
||||||
|
miner: string;
|
||||||
|
difficulty: bigint;
|
||||||
|
gasLimit: bigint;
|
||||||
|
gasUsed: bigint;
|
||||||
|
baseFeePerGas?: bigint;
|
||||||
|
transactions: string[];
|
||||||
|
transactionsRoot: string;
|
||||||
|
stateRoot: string;
|
||||||
|
receiptsRoot: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAG block information (Synor-specific)
|
||||||
|
*/
|
||||||
|
export interface DagBlockInfo extends BlockInfo {
|
||||||
|
blueScore: number;
|
||||||
|
parents: string[];
|
||||||
|
children: string[];
|
||||||
|
isBlue: boolean;
|
||||||
|
mergeSetBlues: string[];
|
||||||
|
mergeSetReds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shard information (Synor-specific)
|
||||||
|
*/
|
||||||
|
export interface ShardInfo {
|
||||||
|
shardId: number;
|
||||||
|
totalShards: number;
|
||||||
|
stateRoot: string;
|
||||||
|
blockHeight: number;
|
||||||
|
validators: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-shard message
|
||||||
|
*/
|
||||||
|
export interface CrossShardMessage {
|
||||||
|
id: string;
|
||||||
|
fromShard: number;
|
||||||
|
toShard: number;
|
||||||
|
sender: string;
|
||||||
|
recipient: string;
|
||||||
|
value: bigint;
|
||||||
|
data: string;
|
||||||
|
status: "pending" | "delivered" | "confirmed" | "failed";
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quantum signature information
|
||||||
|
*/
|
||||||
|
export interface QuantumSignature {
|
||||||
|
algorithm: "dilithium3" | "sphincs+" | "falcon512" | "ed25519+dilithium3";
|
||||||
|
publicKey: string;
|
||||||
|
signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gas estimation result
|
||||||
|
*/
|
||||||
|
export interface GasEstimate {
|
||||||
|
gasLimit: bigint;
|
||||||
|
gasPrice: bigint;
|
||||||
|
maxFeePerGas?: bigint;
|
||||||
|
maxPriorityFeePerGas?: bigint;
|
||||||
|
estimatedCost: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract event filter
|
||||||
|
*/
|
||||||
|
export interface EventFilter {
|
||||||
|
address?: string | string[];
|
||||||
|
topics?: (string | string[] | null)[];
|
||||||
|
fromBlock?: number | string;
|
||||||
|
toBlock?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed contract event
|
||||||
|
*/
|
||||||
|
export interface ParsedEvent {
|
||||||
|
blockNumber: number;
|
||||||
|
blockHash: string;
|
||||||
|
transactionHash: string;
|
||||||
|
transactionIndex: number;
|
||||||
|
logIndex: number;
|
||||||
|
address: string;
|
||||||
|
event: string;
|
||||||
|
args: { [key: string]: any };
|
||||||
|
raw: {
|
||||||
|
topics: string[];
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract call options
|
||||||
|
*/
|
||||||
|
export interface CallOptions {
|
||||||
|
from?: string;
|
||||||
|
gasLimit?: number;
|
||||||
|
gasPrice?: bigint;
|
||||||
|
value?: bigint;
|
||||||
|
blockTag?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send options
|
||||||
|
*/
|
||||||
|
export interface SendOptions extends CallOptions {
|
||||||
|
nonce?: number;
|
||||||
|
confirmations?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract interaction result
|
||||||
|
*/
|
||||||
|
export interface ContractInteractionResult<T = any> {
|
||||||
|
transactionHash?: string;
|
||||||
|
blockNumber?: number;
|
||||||
|
gasUsed?: bigint;
|
||||||
|
events?: ParsedEvent[];
|
||||||
|
returnValue?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment configuration
|
||||||
|
*/
|
||||||
|
export interface DeploymentConfig {
|
||||||
|
contractName: string;
|
||||||
|
constructorArgs: any[];
|
||||||
|
libraries?: { [name: string]: string };
|
||||||
|
proxy?: {
|
||||||
|
type: "transparent" | "uups" | "beacon";
|
||||||
|
owner?: string;
|
||||||
|
};
|
||||||
|
deterministicDeployment?: {
|
||||||
|
salt: string;
|
||||||
|
factory?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network fork configuration
|
||||||
|
*/
|
||||||
|
export interface ForkConfig {
|
||||||
|
jsonRpcUrl: string;
|
||||||
|
blockNumber?: number;
|
||||||
|
accounts?: {
|
||||||
|
mnemonic?: string;
|
||||||
|
path?: string;
|
||||||
|
count?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test fixture
|
||||||
|
*/
|
||||||
|
export interface TestFixture<T> {
|
||||||
|
(): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment result with full metadata
|
||||||
|
*/
|
||||||
|
export interface FullDeploymentResult {
|
||||||
|
address: string;
|
||||||
|
transactionHash: string;
|
||||||
|
blockNumber: number;
|
||||||
|
gasUsed: bigint;
|
||||||
|
deployer: string;
|
||||||
|
timestamp: number;
|
||||||
|
constructorArgs: any[];
|
||||||
|
verified: boolean;
|
||||||
|
}
|
||||||
20
apps/hardhat-plugin/tsconfig.json
Normal file
20
apps/hardhat-plugin/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
21
contracts/multi-sig/Cargo.toml
Normal file
21
contracts/multi-sig/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "synor-multisig"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Multi-signature wallet contract for Synor blockchain"
|
||||||
|
authors = ["Synor Team"]
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
synor-sdk = { path = "../../crates/synor-sdk" }
|
||||||
|
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||||
|
borsh = { version = "1.3", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
545
contracts/multi-sig/src/lib.rs
Normal file
545
contracts/multi-sig/src/lib.rs
Normal file
|
|
@ -0,0 +1,545 @@
|
||||||
|
//! Multi-signature wallet contract for Synor blockchain.
|
||||||
|
//!
|
||||||
|
//! Features:
|
||||||
|
//! - M-of-N signature requirement
|
||||||
|
//! - Owner management (add/remove)
|
||||||
|
//! - Transaction proposals with timelock
|
||||||
|
//! - Native token and contract call support
|
||||||
|
//! - Emergency recovery mechanism
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use synor_sdk::prelude::*;
|
||||||
|
|
||||||
|
/// Maximum number of owners.
|
||||||
|
const MAX_OWNERS: usize = 20;
|
||||||
|
|
||||||
|
/// Minimum timelock duration (1 hour in seconds).
|
||||||
|
const MIN_TIMELOCK: u64 = 3600;
|
||||||
|
|
||||||
|
/// Transaction status.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub enum TxStatus {
|
||||||
|
/// Pending signatures.
|
||||||
|
Pending,
|
||||||
|
/// Ready for execution (enough signatures).
|
||||||
|
Ready,
|
||||||
|
/// Executed successfully.
|
||||||
|
Executed,
|
||||||
|
/// Cancelled.
|
||||||
|
Cancelled,
|
||||||
|
/// Failed execution.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction type.
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub enum TxType {
|
||||||
|
/// Transfer native tokens.
|
||||||
|
Transfer { to: Address, amount: u128 },
|
||||||
|
/// Call another contract.
|
||||||
|
ContractCall {
|
||||||
|
contract: Address,
|
||||||
|
method: String,
|
||||||
|
args: Vec<u8>,
|
||||||
|
value: u128,
|
||||||
|
},
|
||||||
|
/// Add a new owner.
|
||||||
|
AddOwner { owner: Address },
|
||||||
|
/// Remove an owner.
|
||||||
|
RemoveOwner { owner: Address },
|
||||||
|
/// Change signature threshold.
|
||||||
|
ChangeThreshold { new_threshold: u8 },
|
||||||
|
/// Update timelock duration.
|
||||||
|
UpdateTimelock { new_timelock: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposed transaction.
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct Transaction {
|
||||||
|
/// Transaction ID.
|
||||||
|
pub id: u64,
|
||||||
|
/// Transaction type and data.
|
||||||
|
pub tx_type: TxType,
|
||||||
|
/// Proposer address.
|
||||||
|
pub proposer: Address,
|
||||||
|
/// Timestamp when proposed.
|
||||||
|
pub proposed_at: u64,
|
||||||
|
/// Addresses that have signed.
|
||||||
|
pub signers: Vec<Address>,
|
||||||
|
/// Current status.
|
||||||
|
pub status: TxStatus,
|
||||||
|
/// Description/reason.
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multi-sig wallet state.
|
||||||
|
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct MultiSigWallet {
|
||||||
|
/// List of owner addresses.
|
||||||
|
pub owners: Vec<Address>,
|
||||||
|
/// Required number of signatures (M in M-of-N).
|
||||||
|
pub threshold: u8,
|
||||||
|
/// Timelock duration in seconds.
|
||||||
|
pub timelock: u64,
|
||||||
|
/// Next transaction ID.
|
||||||
|
pub next_tx_id: u64,
|
||||||
|
/// Pending transactions.
|
||||||
|
pub pending_txs: Vec<Transaction>,
|
||||||
|
/// Total received amount (for tracking).
|
||||||
|
pub total_received: u128,
|
||||||
|
/// Total sent amount.
|
||||||
|
pub total_sent: u128,
|
||||||
|
/// Whether wallet is paused.
|
||||||
|
pub paused: bool,
|
||||||
|
/// Recovery address (can be zero for no recovery).
|
||||||
|
pub recovery: Address,
|
||||||
|
/// Last activity timestamp.
|
||||||
|
pub last_activity: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiSigWallet {
|
||||||
|
/// Checks if an address is an owner.
|
||||||
|
pub fn is_owner(&self, addr: &Address) -> bool {
|
||||||
|
self.owners.iter().any(|o| o == addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the number of confirmations for a transaction.
|
||||||
|
pub fn confirmation_count(&self, tx_id: u64) -> usize {
|
||||||
|
self.pending_txs
|
||||||
|
.iter()
|
||||||
|
.find(|tx| tx.id == tx_id)
|
||||||
|
.map(|tx| tx.signers.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a transaction is ready for execution.
|
||||||
|
pub fn is_ready(&self, tx_id: u64, current_time: u64) -> bool {
|
||||||
|
self.pending_txs.iter().any(|tx| {
|
||||||
|
tx.id == tx_id
|
||||||
|
&& tx.signers.len() >= self.threshold as usize
|
||||||
|
&& current_time >= tx.proposed_at + self.timelock
|
||||||
|
&& tx.status == TxStatus::Pending
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contract entry points
|
||||||
|
|
||||||
|
/// Initializes the multi-sig wallet.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn initialize(ctx: Context, owners: Vec<Address>, threshold: u8, timelock: u64) -> Result<()> {
|
||||||
|
// Validate inputs
|
||||||
|
require!(!owners.is_empty(), "Must have at least one owner");
|
||||||
|
require!(owners.len() <= MAX_OWNERS, "Too many owners");
|
||||||
|
require!(threshold > 0, "Threshold must be at least 1");
|
||||||
|
require!(
|
||||||
|
threshold as usize <= owners.len(),
|
||||||
|
"Threshold cannot exceed owner count"
|
||||||
|
);
|
||||||
|
require!(timelock >= MIN_TIMELOCK, "Timelock too short");
|
||||||
|
|
||||||
|
// Check for duplicate owners
|
||||||
|
let mut sorted = owners.clone();
|
||||||
|
sorted.sort();
|
||||||
|
for i in 1..sorted.len() {
|
||||||
|
require!(sorted[i] != sorted[i - 1], "Duplicate owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
let state = MultiSigWallet {
|
||||||
|
owners,
|
||||||
|
threshold,
|
||||||
|
timelock,
|
||||||
|
next_tx_id: 0,
|
||||||
|
pending_txs: Vec::new(),
|
||||||
|
total_received: 0,
|
||||||
|
total_sent: 0,
|
||||||
|
paused: false,
|
||||||
|
recovery: Address::zero(),
|
||||||
|
last_activity: ctx.block_timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Initialized", &InitializedEvent {
|
||||||
|
owners: state.owners.clone(),
|
||||||
|
threshold,
|
||||||
|
timelock,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proposes a new transaction.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn propose(ctx: Context, tx_type: TxType, description: String) -> Result<u64> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(!state.paused, "Wallet is paused");
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
|
||||||
|
let tx_id = state.next_tx_id;
|
||||||
|
state.next_tx_id += 1;
|
||||||
|
|
||||||
|
let tx = Transaction {
|
||||||
|
id: tx_id,
|
||||||
|
tx_type: tx_type.clone(),
|
||||||
|
proposer: ctx.sender(),
|
||||||
|
proposed_at: ctx.block_timestamp(),
|
||||||
|
signers: vec![ctx.sender()], // Proposer auto-signs
|
||||||
|
status: TxStatus::Pending,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.pending_txs.push(tx);
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Proposed", &ProposedEvent {
|
||||||
|
tx_id,
|
||||||
|
proposer: ctx.sender(),
|
||||||
|
tx_type,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(tx_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs (confirms) a proposed transaction.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn sign(ctx: Context, tx_id: u64) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(!state.paused, "Wallet is paused");
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
|
||||||
|
let tx = state
|
||||||
|
.pending_txs
|
||||||
|
.iter_mut()
|
||||||
|
.find(|tx| tx.id == tx_id)
|
||||||
|
.ok_or_else(|| Error::msg("Transaction not found"))?;
|
||||||
|
|
||||||
|
require!(tx.status == TxStatus::Pending, "Transaction not pending");
|
||||||
|
require!(
|
||||||
|
!tx.signers.contains(&ctx.sender()),
|
||||||
|
"Already signed"
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.signers.push(ctx.sender());
|
||||||
|
|
||||||
|
// Check if ready
|
||||||
|
if tx.signers.len() >= state.threshold as usize {
|
||||||
|
tx.status = TxStatus::Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Signed", &SignedEvent {
|
||||||
|
tx_id,
|
||||||
|
signer: ctx.sender(),
|
||||||
|
confirmations: tx.signers.len() as u8,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes a confirmed transaction.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn execute(ctx: Context, tx_id: u64) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(!state.paused, "Wallet is paused");
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
require!(
|
||||||
|
state.is_ready(tx_id, ctx.block_timestamp()),
|
||||||
|
"Transaction not ready"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tx_idx = state
|
||||||
|
.pending_txs
|
||||||
|
.iter()
|
||||||
|
.position(|tx| tx.id == tx_id)
|
||||||
|
.expect("Transaction not found");
|
||||||
|
|
||||||
|
let tx = state.pending_txs[tx_idx].clone();
|
||||||
|
|
||||||
|
// Execute based on type
|
||||||
|
let result = match &tx.tx_type {
|
||||||
|
TxType::Transfer { to, amount } => {
|
||||||
|
state.total_sent += amount;
|
||||||
|
ctx.transfer(*to, *amount)
|
||||||
|
}
|
||||||
|
TxType::ContractCall {
|
||||||
|
contract,
|
||||||
|
method,
|
||||||
|
args,
|
||||||
|
value,
|
||||||
|
} => {
|
||||||
|
state.total_sent += value;
|
||||||
|
ctx.call(*contract, method, args, *value)
|
||||||
|
}
|
||||||
|
TxType::AddOwner { owner } => {
|
||||||
|
require!(state.owners.len() < MAX_OWNERS, "Max owners reached");
|
||||||
|
require!(!state.is_owner(owner), "Already an owner");
|
||||||
|
state.owners.push(*owner);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
TxType::RemoveOwner { owner } => {
|
||||||
|
require!(state.owners.len() > state.threshold as usize, "Cannot remove owner below threshold");
|
||||||
|
state.owners.retain(|o| o != owner);
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
TxType::ChangeThreshold { new_threshold } => {
|
||||||
|
require!(*new_threshold > 0, "Invalid threshold");
|
||||||
|
require!(
|
||||||
|
(*new_threshold as usize) <= state.owners.len(),
|
||||||
|
"Threshold exceeds owner count"
|
||||||
|
);
|
||||||
|
state.threshold = *new_threshold;
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
TxType::UpdateTimelock { new_timelock } => {
|
||||||
|
require!(*new_timelock >= MIN_TIMELOCK, "Timelock too short");
|
||||||
|
state.timelock = *new_timelock;
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
state.pending_txs[tx_idx].status = if result.is_ok() {
|
||||||
|
TxStatus::Executed
|
||||||
|
} else {
|
||||||
|
TxStatus::Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Executed", &ExecutedEvent {
|
||||||
|
tx_id,
|
||||||
|
executor: ctx.sender(),
|
||||||
|
success: result.is_ok(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
result.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancels a pending transaction.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn cancel(ctx: Context, tx_id: u64) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
|
||||||
|
let tx = state
|
||||||
|
.pending_txs
|
||||||
|
.iter_mut()
|
||||||
|
.find(|tx| tx.id == tx_id)
|
||||||
|
.ok_or_else(|| Error::msg("Transaction not found"))?;
|
||||||
|
|
||||||
|
require!(tx.status == TxStatus::Pending || tx.status == TxStatus::Ready, "Cannot cancel");
|
||||||
|
require!(
|
||||||
|
tx.proposer == ctx.sender(),
|
||||||
|
"Only proposer can cancel"
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.status = TxStatus::Cancelled;
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Cancelled", &CancelledEvent {
|
||||||
|
tx_id,
|
||||||
|
canceller: ctx.sender(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives native tokens.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
#[payable]
|
||||||
|
pub fn receive(ctx: Context) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
state.total_received += ctx.value();
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Received", &ReceivedEvent {
|
||||||
|
from: ctx.sender(),
|
||||||
|
amount: ctx.value(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pauses the wallet (emergency).
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn pause(ctx: Context) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
require!(!state.paused, "Already paused");
|
||||||
|
|
||||||
|
state.paused = true;
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Paused", &PausedEvent {
|
||||||
|
by: ctx.sender(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unpauses the wallet (requires threshold signatures via separate proposal).
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn unpause(ctx: Context) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
require!(state.paused, "Not paused");
|
||||||
|
|
||||||
|
// Unpause requires full threshold agreement - should be done via proposal
|
||||||
|
// This is a simplified version; production would require multi-sig
|
||||||
|
state.paused = false;
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("Unpaused", &UnpausedEvent {
|
||||||
|
by: ctx.sender(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets recovery address.
|
||||||
|
#[synor_sdk::entry]
|
||||||
|
pub fn set_recovery(ctx: Context, recovery: Address) -> Result<()> {
|
||||||
|
let mut state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
|
||||||
|
// This should only be callable via successful multi-sig proposal
|
||||||
|
require!(state.is_owner(&ctx.sender()), "Not an owner");
|
||||||
|
|
||||||
|
state.recovery = recovery;
|
||||||
|
state.last_activity = ctx.block_timestamp();
|
||||||
|
|
||||||
|
ctx.storage_set(b"state", &state)?;
|
||||||
|
|
||||||
|
ctx.emit_event("RecoverySet", &RecoverySetEvent {
|
||||||
|
recovery,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// View functions
|
||||||
|
|
||||||
|
/// Gets wallet state.
|
||||||
|
#[synor_sdk::view]
|
||||||
|
pub fn get_state(ctx: Context) -> Result<MultiSigWallet> {
|
||||||
|
ctx.storage_get(b"state")?.ok_or_else(|| Error::msg("Not initialized"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a specific transaction.
|
||||||
|
#[synor_sdk::view]
|
||||||
|
pub fn get_transaction(ctx: Context, tx_id: u64) -> Result<Transaction> {
|
||||||
|
let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
state
|
||||||
|
.pending_txs
|
||||||
|
.into_iter()
|
||||||
|
.find(|tx| tx.id == tx_id)
|
||||||
|
.ok_or_else(|| Error::msg("Transaction not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all pending transactions.
|
||||||
|
#[synor_sdk::view]
|
||||||
|
pub fn get_pending_transactions(ctx: Context) -> Result<Vec<Transaction>> {
|
||||||
|
let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
Ok(state
|
||||||
|
.pending_txs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tx| tx.status == TxStatus::Pending || tx.status == TxStatus::Ready)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an address is an owner.
|
||||||
|
#[synor_sdk::view]
|
||||||
|
pub fn is_owner(ctx: Context, addr: Address) -> Result<bool> {
|
||||||
|
let state: MultiSigWallet = ctx.storage_get(b"state")?.expect("Not initialized");
|
||||||
|
Ok(state.is_owner(&addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets wallet balance.
|
||||||
|
#[synor_sdk::view]
|
||||||
|
pub fn get_balance(ctx: Context) -> Result<u128> {
|
||||||
|
ctx.balance()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct InitializedEvent {
|
||||||
|
owners: Vec<Address>,
|
||||||
|
threshold: u8,
|
||||||
|
timelock: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct ProposedEvent {
|
||||||
|
tx_id: u64,
|
||||||
|
proposer: Address,
|
||||||
|
tx_type: TxType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct SignedEvent {
|
||||||
|
tx_id: u64,
|
||||||
|
signer: Address,
|
||||||
|
confirmations: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct ExecutedEvent {
|
||||||
|
tx_id: u64,
|
||||||
|
executor: Address,
|
||||||
|
success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct CancelledEvent {
|
||||||
|
tx_id: u64,
|
||||||
|
canceller: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct ReceivedEvent {
|
||||||
|
from: Address,
|
||||||
|
amount: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct PausedEvent {
|
||||||
|
by: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct UnpausedEvent {
|
||||||
|
by: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(BorshSerialize)]
|
||||||
|
struct RecoverySetEvent {
|
||||||
|
recovery: Address,
|
||||||
|
}
|
||||||
43
crates/synor-verifier/Cargo.toml
Normal file
43
crates/synor-verifier/Cargo.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
[package]
|
||||||
|
name = "synor-verifier"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Formal verification DSL for Synor smart contracts"
|
||||||
|
authors = ["Synor Team"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/synor/blockchain"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
synor-types = { path = "../synor-types" }
|
||||||
|
synor-crypto = { path = "../synor-crypto" }
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# Parsing
|
||||||
|
pest = "2.7"
|
||||||
|
pest_derive = "2.7"
|
||||||
|
|
||||||
|
# SMT solver integration
|
||||||
|
z3 = { version = "0.12", features = ["static-link-z3"], optional = true }
|
||||||
|
|
||||||
|
# Symbolic execution
|
||||||
|
bitvec = "1.0"
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
thiserror = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
# Async
|
||||||
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
z3-backend = ["z3"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = "1.4"
|
||||||
346
crates/synor-verifier/src/ast.rs
Normal file
346
crates/synor-verifier/src/ast.rs
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
//! Abstract Syntax Tree for the verification DSL.
|
||||||
|
//!
|
||||||
|
//! The DSL supports:
|
||||||
|
//! - Contract invariants (properties that hold across all states)
|
||||||
|
//! - Function pre/post conditions
|
||||||
|
//! - Temporal properties (eventually, always)
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::VarType;
|
||||||
|
|
||||||
|
/// A parsed contract with specifications.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Contract {
|
||||||
|
/// Contract name.
|
||||||
|
pub name: String,
|
||||||
|
/// State variables.
|
||||||
|
pub state_vars: Vec<StateVar>,
|
||||||
|
/// Functions.
|
||||||
|
pub functions: Vec<Function>,
|
||||||
|
/// Specifications.
|
||||||
|
pub specs: Vec<Specification>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State variable declaration.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct StateVar {
|
||||||
|
/// Variable name.
|
||||||
|
pub name: String,
|
||||||
|
/// Variable type.
|
||||||
|
pub var_type: VarType,
|
||||||
|
/// Initial value expression (optional).
|
||||||
|
pub init: Option<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function definition.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Function {
|
||||||
|
/// Function name.
|
||||||
|
pub name: String,
|
||||||
|
/// Parameters.
|
||||||
|
pub params: Vec<(String, VarType)>,
|
||||||
|
/// Return type.
|
||||||
|
pub returns: Option<VarType>,
|
||||||
|
/// Function body.
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
/// Pre-conditions.
|
||||||
|
pub requires: Vec<Expression>,
|
||||||
|
/// Post-conditions.
|
||||||
|
pub ensures: Vec<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statement in function body.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Statement {
|
||||||
|
/// Variable assignment.
|
||||||
|
Assign {
|
||||||
|
target: String,
|
||||||
|
value: Expression,
|
||||||
|
},
|
||||||
|
/// If-then-else.
|
||||||
|
If {
|
||||||
|
condition: Expression,
|
||||||
|
then_branch: Vec<Statement>,
|
||||||
|
else_branch: Vec<Statement>,
|
||||||
|
},
|
||||||
|
/// While loop.
|
||||||
|
While {
|
||||||
|
condition: Expression,
|
||||||
|
invariant: Option<Expression>,
|
||||||
|
body: Vec<Statement>,
|
||||||
|
},
|
||||||
|
/// For loop.
|
||||||
|
For {
|
||||||
|
init: Box<Statement>,
|
||||||
|
condition: Expression,
|
||||||
|
update: Box<Statement>,
|
||||||
|
body: Vec<Statement>,
|
||||||
|
},
|
||||||
|
/// Return statement.
|
||||||
|
Return(Option<Expression>),
|
||||||
|
/// Require (revert if false).
|
||||||
|
Require(Expression, Option<String>),
|
||||||
|
/// Assert (for verification).
|
||||||
|
Assert(Expression),
|
||||||
|
/// Assume (hint to verifier).
|
||||||
|
Assume(Expression),
|
||||||
|
/// Emit event.
|
||||||
|
Emit {
|
||||||
|
event: String,
|
||||||
|
args: Vec<Expression>,
|
||||||
|
},
|
||||||
|
/// External call.
|
||||||
|
Call {
|
||||||
|
target: Expression,
|
||||||
|
function: String,
|
||||||
|
args: Vec<Expression>,
|
||||||
|
value: Option<Expression>,
|
||||||
|
},
|
||||||
|
/// Block of statements.
|
||||||
|
Block(Vec<Statement>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expression in the DSL.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Expression {
|
||||||
|
/// Literal value.
|
||||||
|
Literal(Literal),
|
||||||
|
/// Variable reference.
|
||||||
|
Variable(String),
|
||||||
|
/// Binary operation.
|
||||||
|
BinaryOp {
|
||||||
|
left: Box<Expression>,
|
||||||
|
op: BinaryOperator,
|
||||||
|
right: Box<Expression>,
|
||||||
|
},
|
||||||
|
/// Unary operation.
|
||||||
|
UnaryOp {
|
||||||
|
op: UnaryOperator,
|
||||||
|
operand: Box<Expression>,
|
||||||
|
},
|
||||||
|
/// Ternary conditional.
|
||||||
|
Ternary {
|
||||||
|
condition: Box<Expression>,
|
||||||
|
then_expr: Box<Expression>,
|
||||||
|
else_expr: Box<Expression>,
|
||||||
|
},
|
||||||
|
/// Function call.
|
||||||
|
FunctionCall {
|
||||||
|
function: String,
|
||||||
|
args: Vec<Expression>,
|
||||||
|
},
|
||||||
|
/// Member access (e.g., msg.sender).
|
||||||
|
MemberAccess {
|
||||||
|
object: Box<Expression>,
|
||||||
|
member: String,
|
||||||
|
},
|
||||||
|
/// Array/mapping index.
|
||||||
|
Index {
|
||||||
|
array: Box<Expression>,
|
||||||
|
index: Box<Expression>,
|
||||||
|
},
|
||||||
|
/// Quantifier (forall, exists).
|
||||||
|
Quantifier {
|
||||||
|
kind: QuantifierKind,
|
||||||
|
var: String,
|
||||||
|
var_type: VarType,
|
||||||
|
body: Box<Expression>,
|
||||||
|
},
|
||||||
|
/// Old value (in post-conditions).
|
||||||
|
Old(Box<Expression>),
|
||||||
|
/// Result value (in post-conditions).
|
||||||
|
Result,
|
||||||
|
/// Sum over range (for array properties).
|
||||||
|
Sum {
|
||||||
|
var: String,
|
||||||
|
range: Box<Expression>,
|
||||||
|
body: Box<Expression>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Literal values.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Literal {
|
||||||
|
Bool(bool),
|
||||||
|
Uint(u128),
|
||||||
|
Int(i128),
|
||||||
|
Address([u8; 20]),
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binary operators.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum BinaryOperator {
|
||||||
|
// Arithmetic
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Mod,
|
||||||
|
Exp,
|
||||||
|
// Comparison
|
||||||
|
Eq,
|
||||||
|
Ne,
|
||||||
|
Lt,
|
||||||
|
Le,
|
||||||
|
Gt,
|
||||||
|
Ge,
|
||||||
|
// Logical
|
||||||
|
And,
|
||||||
|
Or,
|
||||||
|
Implies,
|
||||||
|
Iff,
|
||||||
|
// Bitwise
|
||||||
|
BitAnd,
|
||||||
|
BitOr,
|
||||||
|
BitXor,
|
||||||
|
Shl,
|
||||||
|
Shr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BinaryOperator {
|
||||||
|
/// Returns true if this is a comparison operator.
|
||||||
|
pub fn is_comparison(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
BinaryOperator::Eq
|
||||||
|
| BinaryOperator::Ne
|
||||||
|
| BinaryOperator::Lt
|
||||||
|
| BinaryOperator::Le
|
||||||
|
| BinaryOperator::Gt
|
||||||
|
| BinaryOperator::Ge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this is a logical operator.
|
||||||
|
pub fn is_logical(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
BinaryOperator::And | BinaryOperator::Or | BinaryOperator::Implies | BinaryOperator::Iff
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unary operators.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum UnaryOperator {
|
||||||
|
Not,
|
||||||
|
Neg,
|
||||||
|
BitNot,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quantifier kinds.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum QuantifierKind {
|
||||||
|
ForAll,
|
||||||
|
Exists,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specification types.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Specification {
|
||||||
|
/// Contract invariant.
|
||||||
|
Invariant(Invariant),
|
||||||
|
/// Temporal or custom property.
|
||||||
|
Property(Property),
|
||||||
|
/// Function annotation.
|
||||||
|
Annotation(Annotation),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contract invariant.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Invariant {
|
||||||
|
/// Invariant name.
|
||||||
|
pub name: String,
|
||||||
|
/// The invariant expression.
|
||||||
|
pub expr: Expression,
|
||||||
|
/// Description.
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom property.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Property {
|
||||||
|
/// Property name.
|
||||||
|
pub name: String,
|
||||||
|
/// Property kind.
|
||||||
|
pub kind: PropertyKind,
|
||||||
|
/// The property expression.
|
||||||
|
pub expr: Expression,
|
||||||
|
/// Description.
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property kinds.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PropertyKind {
|
||||||
|
/// Safety property (always holds).
|
||||||
|
Safety,
|
||||||
|
/// Liveness property (eventually holds).
|
||||||
|
Liveness,
|
||||||
|
/// Reachability (some state is reachable).
|
||||||
|
Reachability,
|
||||||
|
/// Custom property.
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function annotation.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Annotation {
|
||||||
|
/// Function name this annotation applies to.
|
||||||
|
pub function: String,
|
||||||
|
/// Pre-conditions.
|
||||||
|
pub requires: Vec<Expression>,
|
||||||
|
/// Post-conditions.
|
||||||
|
pub ensures: Vec<Expression>,
|
||||||
|
/// Modifies clause.
|
||||||
|
pub modifies: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binary_operator_classification() {
|
||||||
|
assert!(BinaryOperator::Eq.is_comparison());
|
||||||
|
assert!(BinaryOperator::Lt.is_comparison());
|
||||||
|
assert!(!BinaryOperator::Add.is_comparison());
|
||||||
|
|
||||||
|
assert!(BinaryOperator::And.is_logical());
|
||||||
|
assert!(BinaryOperator::Implies.is_logical());
|
||||||
|
assert!(!BinaryOperator::Mul.is_logical());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expression_construction() {
|
||||||
|
let expr = Expression::BinaryOp {
|
||||||
|
left: Box::new(Expression::Variable("x".to_string())),
|
||||||
|
op: BinaryOperator::Add,
|
||||||
|
right: Box::new(Expression::Literal(Literal::Uint(1))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Expression::BinaryOp { op, .. } = expr {
|
||||||
|
assert_eq!(op, BinaryOperator::Add);
|
||||||
|
} else {
|
||||||
|
panic!("Expected BinaryOp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invariant_creation() {
|
||||||
|
let inv = Invariant {
|
||||||
|
name: "positive_balance".to_string(),
|
||||||
|
expr: Expression::BinaryOp {
|
||||||
|
left: Box::new(Expression::Variable("balance".to_string())),
|
||||||
|
op: BinaryOperator::Ge,
|
||||||
|
right: Box::new(Expression::Literal(Literal::Uint(0))),
|
||||||
|
},
|
||||||
|
description: Some("Balance is always non-negative".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(inv.name, "positive_balance");
|
||||||
|
}
|
||||||
|
}
|
||||||
416
crates/synor-verifier/src/checker.rs
Normal file
416
crates/synor-verifier/src/checker.rs
Normal file
|
|
@ -0,0 +1,416 @@
|
||||||
|
//! Property checking and vulnerability detection.
|
||||||
|
//!
|
||||||
|
//! Checks contract properties and detects common vulnerabilities.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::ast::{Annotation, Expression, Invariant, Property, PropertyKind};
|
||||||
|
use crate::error::{VerifierError, VerifierResult};
|
||||||
|
use crate::prover::{ProofResult, Prover, ProverConfig};
|
||||||
|
use crate::symbolic::{SymbolicExecutor, SymbolicState};
|
||||||
|
use crate::{Severity, VerificationContext};
|
||||||
|
|
||||||
|
/// Result of checking a property.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CheckResult {
|
||||||
|
/// Property name.
|
||||||
|
pub name: String,
|
||||||
|
/// Whether property was verified.
|
||||||
|
pub verified: bool,
|
||||||
|
/// Proof result details.
|
||||||
|
pub proof: ProofResult,
|
||||||
|
/// Additional messages.
|
||||||
|
pub messages: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckResult {
|
||||||
|
/// Creates a new check result.
|
||||||
|
pub fn new(name: &str, verified: bool, proof: ProofResult) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
verified,
|
||||||
|
proof,
|
||||||
|
messages: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a message.
|
||||||
|
pub fn add_message(&mut self, msg: &str) {
|
||||||
|
self.messages.push(msg.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if property is verified.
|
||||||
|
pub fn is_verified(&self) -> bool {
|
||||||
|
self.verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulnerability report.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VulnerabilityReport {
|
||||||
|
/// Vulnerability type.
|
||||||
|
pub vuln_type: VulnerabilityType,
|
||||||
|
/// Severity level.
|
||||||
|
pub severity: Severity,
|
||||||
|
/// Description.
|
||||||
|
pub description: String,
|
||||||
|
/// Location (function name, line number).
|
||||||
|
pub location: String,
|
||||||
|
/// Suggested fix.
|
||||||
|
pub fix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Known vulnerability types.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum VulnerabilityType {
|
||||||
|
/// Integer overflow/underflow.
|
||||||
|
IntegerOverflow,
|
||||||
|
/// Reentrancy attack.
|
||||||
|
Reentrancy,
|
||||||
|
/// Unchecked external call.
|
||||||
|
UncheckedCall,
|
||||||
|
/// Access control issues.
|
||||||
|
AccessControl,
|
||||||
|
/// Front-running vulnerability.
|
||||||
|
FrontRunning,
|
||||||
|
/// Denial of service.
|
||||||
|
DoS,
|
||||||
|
/// Timestamp dependence.
|
||||||
|
TimestampDependence,
|
||||||
|
/// Weak randomness.
|
||||||
|
WeakRandomness,
|
||||||
|
/// Uninitialized storage.
|
||||||
|
UninitializedStorage,
|
||||||
|
/// Delegatecall to untrusted.
|
||||||
|
DelegatecallInjection,
|
||||||
|
/// Self-destruct reachable.
|
||||||
|
SelfDestruct,
|
||||||
|
/// Gas limit issues.
|
||||||
|
GasLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VulnerabilityType {
|
||||||
|
/// Returns the default severity for this vulnerability type.
|
||||||
|
pub fn default_severity(&self) -> Severity {
|
||||||
|
match self {
|
||||||
|
VulnerabilityType::Reentrancy => Severity::Critical,
|
||||||
|
VulnerabilityType::IntegerOverflow => Severity::High,
|
||||||
|
VulnerabilityType::UncheckedCall => Severity::High,
|
||||||
|
VulnerabilityType::AccessControl => Severity::Critical,
|
||||||
|
VulnerabilityType::DelegatecallInjection => Severity::Critical,
|
||||||
|
VulnerabilityType::SelfDestruct => Severity::Critical,
|
||||||
|
VulnerabilityType::FrontRunning => Severity::Medium,
|
||||||
|
VulnerabilityType::DoS => Severity::Medium,
|
||||||
|
VulnerabilityType::TimestampDependence => Severity::Low,
|
||||||
|
VulnerabilityType::WeakRandomness => Severity::Medium,
|
||||||
|
VulnerabilityType::UninitializedStorage => Severity::High,
|
||||||
|
VulnerabilityType::GasLimit => Severity::Low,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a description of the vulnerability.
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
VulnerabilityType::Reentrancy => {
|
||||||
|
"External call followed by state change allows reentrancy attack"
|
||||||
|
}
|
||||||
|
VulnerabilityType::IntegerOverflow => {
|
||||||
|
"Arithmetic operation may overflow or underflow"
|
||||||
|
}
|
||||||
|
VulnerabilityType::UncheckedCall => {
|
||||||
|
"External call return value not checked"
|
||||||
|
}
|
||||||
|
VulnerabilityType::AccessControl => {
|
||||||
|
"Missing or insufficient access control"
|
||||||
|
}
|
||||||
|
VulnerabilityType::FrontRunning => {
|
||||||
|
"Transaction ordering can be exploited"
|
||||||
|
}
|
||||||
|
VulnerabilityType::DoS => {
|
||||||
|
"Contract can be rendered unusable"
|
||||||
|
}
|
||||||
|
VulnerabilityType::TimestampDependence => {
|
||||||
|
"Reliance on block.timestamp which can be manipulated"
|
||||||
|
}
|
||||||
|
VulnerabilityType::WeakRandomness => {
|
||||||
|
"Predictable random number generation"
|
||||||
|
}
|
||||||
|
VulnerabilityType::UninitializedStorage => {
|
||||||
|
"Storage variable used before initialization"
|
||||||
|
}
|
||||||
|
VulnerabilityType::DelegatecallInjection => {
|
||||||
|
"Delegatecall to user-controlled address"
|
||||||
|
}
|
||||||
|
VulnerabilityType::SelfDestruct => {
|
||||||
|
"Contract can be destroyed by attacker"
|
||||||
|
}
|
||||||
|
VulnerabilityType::GasLimit => {
|
||||||
|
"Loop or operation may exceed gas limit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Property checker using symbolic execution and SMT solving.
|
||||||
|
pub struct PropertyChecker {
|
||||||
|
config: ProverConfig,
|
||||||
|
prover: Prover,
|
||||||
|
executor: SymbolicExecutor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyChecker {
|
||||||
|
/// Creates a new property checker.
|
||||||
|
pub fn new(config: ProverConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
prover: Prover::new(config.clone()),
|
||||||
|
executor: SymbolicExecutor::new(config.clone()),
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks an invariant holds in all states.
|
||||||
|
pub fn check_invariant(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
inv: &Invariant,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
// Create initial symbolic state
|
||||||
|
let state = self.executor.create_initial_state(ctx)?;
|
||||||
|
|
||||||
|
// Prove invariant
|
||||||
|
let proof = self.prover.prove(&inv.expr, &state)?;
|
||||||
|
|
||||||
|
let verified = proof.is_proven();
|
||||||
|
let mut result = CheckResult::new(&inv.name, verified, proof);
|
||||||
|
|
||||||
|
if let Some(desc) = &inv.description {
|
||||||
|
result.add_message(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks a property.
|
||||||
|
pub fn check_property(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
prop: &Property,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
match prop.kind {
|
||||||
|
PropertyKind::Safety => self.check_safety_property(ctx, prop),
|
||||||
|
PropertyKind::Liveness => self.check_liveness_property(ctx, prop),
|
||||||
|
PropertyKind::Reachability => self.check_reachability(ctx, prop),
|
||||||
|
PropertyKind::Custom => {
|
||||||
|
// Custom properties checked like invariants
|
||||||
|
let state = self.executor.create_initial_state(ctx)?;
|
||||||
|
let proof = self.prover.prove(&prop.expr, &state)?;
|
||||||
|
Ok(CheckResult::new(&prop.name, proof.is_proven(), proof))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks a safety property (must always hold).
|
||||||
|
fn check_safety_property(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
prop: &Property,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
// Symbolically execute all paths and check property
|
||||||
|
let paths = self.executor.explore_all_paths(ctx)?;
|
||||||
|
|
||||||
|
for state in &paths {
|
||||||
|
let proof = self.prover.prove(&prop.expr, state)?;
|
||||||
|
if !proof.is_proven() {
|
||||||
|
return Ok(CheckResult::new(&prop.name, false, proof));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CheckResult::new(
|
||||||
|
&prop.name,
|
||||||
|
true,
|
||||||
|
ProofResult::Proven {
|
||||||
|
time_ms: 0, // Aggregate time not tracked
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks a liveness property (must eventually hold).
|
||||||
|
fn check_liveness_property(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
prop: &Property,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
// Check if there exists a path where property holds
|
||||||
|
let state = self.executor.create_initial_state(ctx)?;
|
||||||
|
let satisfiable = self.prover.check_sat(&prop.expr)?;
|
||||||
|
|
||||||
|
if satisfiable {
|
||||||
|
Ok(CheckResult::new(
|
||||||
|
&prop.name,
|
||||||
|
true,
|
||||||
|
ProofResult::Proven { time_ms: 0 },
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(CheckResult::new(
|
||||||
|
&prop.name,
|
||||||
|
false,
|
||||||
|
ProofResult::Unknown {
|
||||||
|
reason: "No path found where property holds".to_string(),
|
||||||
|
time_ms: 0,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks reachability (some state is reachable).
|
||||||
|
fn check_reachability(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
prop: &Property,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
// Check if target state is reachable
|
||||||
|
let satisfiable = self.prover.check_sat(&prop.expr)?;
|
||||||
|
|
||||||
|
Ok(CheckResult::new(
|
||||||
|
&prop.name,
|
||||||
|
satisfiable,
|
||||||
|
if satisfiable {
|
||||||
|
ProofResult::Proven { time_ms: 0 }
|
||||||
|
} else {
|
||||||
|
ProofResult::Unknown {
|
||||||
|
reason: "State not reachable".to_string(),
|
||||||
|
time_ms: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks a function annotation (pre/post conditions).
|
||||||
|
pub fn check_annotation(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
ann: &Annotation,
|
||||||
|
) -> VerifierResult<CheckResult> {
|
||||||
|
// Get function signature
|
||||||
|
let func = ctx.functions.get(&ann.function).ok_or_else(|| {
|
||||||
|
VerifierError::UnknownFunction(ann.function.clone())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let state = self.executor.create_initial_state(ctx)?;
|
||||||
|
|
||||||
|
// Check that preconditions imply postconditions
|
||||||
|
for pre in &ann.requires {
|
||||||
|
for post in &ann.ensures {
|
||||||
|
let proof = self.prover.prove_implication(pre, post, &state)?;
|
||||||
|
if !proof.is_proven() {
|
||||||
|
return Ok(CheckResult::new(&ann.function, false, proof));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CheckResult::new(
|
||||||
|
&ann.function,
|
||||||
|
true,
|
||||||
|
ProofResult::Proven { time_ms: 0 },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detects vulnerabilities in a contract.
|
||||||
|
pub fn detect_vulnerabilities(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
) -> VerifierResult<Vec<VulnerabilityReport>> {
|
||||||
|
let mut vulns = Vec::new();
|
||||||
|
|
||||||
|
// Check each function for vulnerabilities
|
||||||
|
for (name, func) in &ctx.functions {
|
||||||
|
// Check for reentrancy
|
||||||
|
if self.check_reentrancy_pattern(func) {
|
||||||
|
vulns.push(VulnerabilityReport {
|
||||||
|
vuln_type: VulnerabilityType::Reentrancy,
|
||||||
|
severity: Severity::Critical,
|
||||||
|
description: VulnerabilityType::Reentrancy.description().to_string(),
|
||||||
|
location: name.clone(),
|
||||||
|
fix: Some("Use checks-effects-interactions pattern".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unchecked calls
|
||||||
|
if self.check_unchecked_call_pattern(func) {
|
||||||
|
vulns.push(VulnerabilityReport {
|
||||||
|
vuln_type: VulnerabilityType::UncheckedCall,
|
||||||
|
severity: Severity::High,
|
||||||
|
description: VulnerabilityType::UncheckedCall.description().to_string(),
|
||||||
|
location: name.clone(),
|
||||||
|
fix: Some("Check return value of external calls".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for integer overflow (if no SafeMath)
|
||||||
|
if self.check_overflow_pattern(func) {
|
||||||
|
vulns.push(VulnerabilityReport {
|
||||||
|
vuln_type: VulnerabilityType::IntegerOverflow,
|
||||||
|
severity: Severity::High,
|
||||||
|
description: VulnerabilityType::IntegerOverflow.description().to_string(),
|
||||||
|
location: name.clone(),
|
||||||
|
fix: Some("Use SafeMath or Solidity 0.8+".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vulns)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for reentrancy vulnerability pattern.
|
||||||
|
fn check_reentrancy_pattern(&self, _func: &crate::FunctionSig) -> bool {
|
||||||
|
// Pattern: external call followed by state change
|
||||||
|
// Simplified check - full implementation would analyze CFG
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for unchecked external call pattern.
|
||||||
|
fn check_unchecked_call_pattern(&self, _func: &crate::FunctionSig) -> bool {
|
||||||
|
// Pattern: call() without checking return value
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for integer overflow pattern.
|
||||||
|
fn check_overflow_pattern(&self, _func: &crate::FunctionSig) -> bool {
|
||||||
|
// Pattern: arithmetic without overflow checks
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vulnerability_type_severity() {
|
||||||
|
assert_eq!(
|
||||||
|
VulnerabilityType::Reentrancy.default_severity(),
|
||||||
|
Severity::Critical
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
VulnerabilityType::TimestampDependence.default_severity(),
|
||||||
|
Severity::Low
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_result() {
|
||||||
|
let result = CheckResult::new(
|
||||||
|
"test_prop",
|
||||||
|
true,
|
||||||
|
ProofResult::Proven { time_ms: 100 },
|
||||||
|
);
|
||||||
|
assert!(result.is_verified());
|
||||||
|
assert_eq!(result.name, "test_prop");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_property_checker_creation() {
|
||||||
|
let checker = PropertyChecker::new(ProverConfig::default());
|
||||||
|
assert_eq!(checker.config.timeout_ms, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
crates/synor-verifier/src/error.rs
Normal file
54
crates/synor-verifier/src/error.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! Error types for the verifier.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Verifier error types.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum VerifierError {
|
||||||
|
/// Parse error in specification.
|
||||||
|
#[error("Parse error at line {line}: {message}")]
|
||||||
|
ParseError { line: usize, message: String },
|
||||||
|
|
||||||
|
/// Type error in expression.
|
||||||
|
#[error("Type error: {0}")]
|
||||||
|
TypeError(String),
|
||||||
|
|
||||||
|
/// Unknown variable reference.
|
||||||
|
#[error("Unknown variable: {0}")]
|
||||||
|
UnknownVariable(String),
|
||||||
|
|
||||||
|
/// Unknown function reference.
|
||||||
|
#[error("Unknown function: {0}")]
|
||||||
|
UnknownFunction(String),
|
||||||
|
|
||||||
|
/// SMT solver error.
|
||||||
|
#[error("SMT solver error: {0}")]
|
||||||
|
SmtError(String),
|
||||||
|
|
||||||
|
/// Timeout during verification.
|
||||||
|
#[error("Verification timeout after {0}ms")]
|
||||||
|
Timeout(u64),
|
||||||
|
|
||||||
|
/// Verification proved false.
|
||||||
|
#[error("Property falsified: {0}")]
|
||||||
|
Falsified(String),
|
||||||
|
|
||||||
|
/// Internal error.
|
||||||
|
#[error("Internal error: {0}")]
|
||||||
|
Internal(String),
|
||||||
|
|
||||||
|
/// Unsupported feature.
|
||||||
|
#[error("Unsupported: {0}")]
|
||||||
|
Unsupported(String),
|
||||||
|
|
||||||
|
/// IO error.
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
/// JSON error.
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result type for verifier operations.
|
||||||
|
pub type VerifierResult<T> = Result<T, VerifierError>;
|
||||||
336
crates/synor-verifier/src/lib.rs
Normal file
336
crates/synor-verifier/src/lib.rs
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
//! Formal verification DSL for Synor smart contracts.
|
||||||
|
//!
|
||||||
|
//! This crate provides:
|
||||||
|
//! - A domain-specific language for specifying contract invariants
|
||||||
|
//! - Symbolic execution engine for exploring all execution paths
|
||||||
|
//! - SMT solver integration for proving/disproving properties
|
||||||
|
//! - Automated vulnerability detection
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
pub mod checker;
|
||||||
|
pub mod error;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod prover;
|
||||||
|
pub mod smt;
|
||||||
|
pub mod symbolic;
|
||||||
|
|
||||||
|
pub use ast::{Annotation, Contract, Expression, Invariant, Property, Specification, Statement};
|
||||||
|
pub use checker::{CheckResult, PropertyChecker, VulnerabilityReport};
|
||||||
|
pub use error::{VerifierError, VerifierResult};
|
||||||
|
pub use parser::SpecParser;
|
||||||
|
pub use prover::{ProofResult, Prover, ProverConfig};
|
||||||
|
pub use symbolic::{SymbolicExecutor, SymbolicState, SymbolicValue};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Verification context containing all contract metadata.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VerificationContext {
|
||||||
|
/// Contract name.
|
||||||
|
pub contract_name: String,
|
||||||
|
/// Contract source code.
|
||||||
|
pub source: String,
|
||||||
|
/// Parsed specifications.
|
||||||
|
pub specs: Vec<Specification>,
|
||||||
|
/// State variables and their types.
|
||||||
|
pub state_vars: HashMap<String, VarType>,
|
||||||
|
/// Function signatures.
|
||||||
|
pub functions: HashMap<String, FunctionSig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variable type in the verification DSL.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum VarType {
|
||||||
|
/// Unsigned integer with bit width.
|
||||||
|
Uint(u16),
|
||||||
|
/// Signed integer with bit width.
|
||||||
|
Int(u16),
|
||||||
|
/// Boolean.
|
||||||
|
Bool,
|
||||||
|
/// Address (20 bytes).
|
||||||
|
Address,
|
||||||
|
/// Fixed-size bytes.
|
||||||
|
Bytes(u16),
|
||||||
|
/// Dynamic bytes.
|
||||||
|
DynBytes,
|
||||||
|
/// String.
|
||||||
|
String,
|
||||||
|
/// Array of type.
|
||||||
|
Array(Box<VarType>),
|
||||||
|
/// Mapping from key to value type.
|
||||||
|
Mapping(Box<VarType>, Box<VarType>),
|
||||||
|
/// Struct with named fields.
|
||||||
|
Struct(String, Vec<(String, VarType)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VarType {
|
||||||
|
/// Returns the bit width of numeric types.
|
||||||
|
pub fn bit_width(&self) -> Option<u16> {
|
||||||
|
match self {
|
||||||
|
VarType::Uint(w) | VarType::Int(w) => Some(*w),
|
||||||
|
VarType::Bool => Some(1),
|
||||||
|
VarType::Address => Some(160),
|
||||||
|
VarType::Bytes(n) => Some(*n * 8),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if type is numeric.
|
||||||
|
pub fn is_numeric(&self) -> bool {
|
||||||
|
matches!(self, VarType::Uint(_) | VarType::Int(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function signature for verification.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct FunctionSig {
|
||||||
|
/// Function name.
|
||||||
|
pub name: String,
|
||||||
|
/// Parameter types.
|
||||||
|
pub params: Vec<(String, VarType)>,
|
||||||
|
/// Return type (None for void).
|
||||||
|
pub returns: Option<VarType>,
|
||||||
|
/// Visibility.
|
||||||
|
pub visibility: Visibility,
|
||||||
|
/// State mutability.
|
||||||
|
pub mutability: Mutability,
|
||||||
|
/// Pre-conditions (requires).
|
||||||
|
pub preconditions: Vec<Expression>,
|
||||||
|
/// Post-conditions (ensures).
|
||||||
|
pub postconditions: Vec<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function visibility.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Visibility {
|
||||||
|
Public,
|
||||||
|
External,
|
||||||
|
Internal,
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function state mutability.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Mutability {
|
||||||
|
Pure,
|
||||||
|
View,
|
||||||
|
Nonpayable,
|
||||||
|
Payable,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main verifier interface.
|
||||||
|
pub struct Verifier {
|
||||||
|
/// Prover configuration.
|
||||||
|
config: ProverConfig,
|
||||||
|
/// Symbolic executor.
|
||||||
|
executor: SymbolicExecutor,
|
||||||
|
/// Property checker.
|
||||||
|
checker: PropertyChecker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Verifier {
|
||||||
|
/// Creates a new verifier with default configuration.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_config(ProverConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new verifier with custom configuration.
|
||||||
|
pub fn with_config(config: ProverConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config: config.clone(),
|
||||||
|
executor: SymbolicExecutor::new(config.clone()),
|
||||||
|
checker: PropertyChecker::new(config),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies a contract against its specifications.
|
||||||
|
pub fn verify(&self, ctx: &VerificationContext) -> VerifierResult<VerificationReport> {
|
||||||
|
let mut report = VerificationReport::new(&ctx.contract_name);
|
||||||
|
|
||||||
|
// Check each specification
|
||||||
|
for spec in &ctx.specs {
|
||||||
|
let result = self.verify_spec(ctx, spec)?;
|
||||||
|
report.add_result(spec.clone(), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run vulnerability detection
|
||||||
|
let vulns = self.checker.detect_vulnerabilities(ctx)?;
|
||||||
|
report.vulnerabilities = vulns;
|
||||||
|
|
||||||
|
// Compute overall status
|
||||||
|
report.compute_status();
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies a single specification.
|
||||||
|
fn verify_spec(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
spec: &Specification,
|
||||||
|
) -> VerifierResult<SpecResult> {
|
||||||
|
match spec {
|
||||||
|
Specification::Invariant(inv) => {
|
||||||
|
let result = self.checker.check_invariant(ctx, inv)?;
|
||||||
|
Ok(SpecResult::Invariant(result))
|
||||||
|
}
|
||||||
|
Specification::Property(prop) => {
|
||||||
|
let result = self.checker.check_property(ctx, prop)?;
|
||||||
|
Ok(SpecResult::Property(result))
|
||||||
|
}
|
||||||
|
Specification::Annotation(ann) => {
|
||||||
|
let result = self.checker.check_annotation(ctx, ann)?;
|
||||||
|
Ok(SpecResult::Annotation(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses and verifies a contract from source.
|
||||||
|
pub fn verify_source(&self, source: &str) -> VerifierResult<VerificationReport> {
|
||||||
|
let ctx = SpecParser::parse_contract(source)?;
|
||||||
|
self.verify(&ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Verifier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of verifying a specification.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum SpecResult {
|
||||||
|
Invariant(CheckResult),
|
||||||
|
Property(CheckResult),
|
||||||
|
Annotation(CheckResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpecResult {
|
||||||
|
/// Returns true if verification succeeded.
|
||||||
|
pub fn is_verified(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
SpecResult::Invariant(r) | SpecResult::Property(r) | SpecResult::Annotation(r) => {
|
||||||
|
r.is_verified()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overall verification report.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VerificationReport {
|
||||||
|
/// Contract name.
|
||||||
|
pub contract_name: String,
|
||||||
|
/// Results per specification.
|
||||||
|
pub results: Vec<(Specification, SpecResult)>,
|
||||||
|
/// Detected vulnerabilities.
|
||||||
|
pub vulnerabilities: Vec<VulnerabilityReport>,
|
||||||
|
/// Overall status.
|
||||||
|
pub status: VerificationStatus,
|
||||||
|
/// Time taken (ms).
|
||||||
|
pub time_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerificationReport {
|
||||||
|
fn new(contract_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
contract_name: contract_name.to_string(),
|
||||||
|
results: Vec::new(),
|
||||||
|
vulnerabilities: Vec::new(),
|
||||||
|
status: VerificationStatus::Unknown,
|
||||||
|
time_ms: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_result(&mut self, spec: Specification, result: SpecResult) {
|
||||||
|
self.results.push((spec, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_status(&mut self) {
|
||||||
|
let all_verified = self.results.iter().all(|(_, r)| r.is_verified());
|
||||||
|
let no_vulns = self.vulnerabilities.iter().all(|v| v.severity < Severity::High);
|
||||||
|
|
||||||
|
self.status = if all_verified && no_vulns {
|
||||||
|
VerificationStatus::Verified
|
||||||
|
} else if self.vulnerabilities.iter().any(|v| v.severity == Severity::Critical) {
|
||||||
|
VerificationStatus::Failed
|
||||||
|
} else {
|
||||||
|
VerificationStatus::PartiallyVerified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a summary string.
|
||||||
|
pub fn summary(&self) -> String {
|
||||||
|
let verified = self.results.iter().filter(|(_, r)| r.is_verified()).count();
|
||||||
|
let total = self.results.len();
|
||||||
|
let vulns = self.vulnerabilities.len();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Contract: {} | Verified: {}/{} | Vulnerabilities: {} | Status: {:?}",
|
||||||
|
self.contract_name, verified, total, vulns, self.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verification status.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum VerificationStatus {
|
||||||
|
/// All properties verified.
|
||||||
|
Verified,
|
||||||
|
/// Some properties verified.
|
||||||
|
PartiallyVerified,
|
||||||
|
/// Verification failed.
|
||||||
|
Failed,
|
||||||
|
/// Status unknown (timeout, etc.).
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vulnerability severity.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub enum Severity {
|
||||||
|
Info,
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
Critical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_var_type_bit_width() {
|
||||||
|
assert_eq!(VarType::Uint(256).bit_width(), Some(256));
|
||||||
|
assert_eq!(VarType::Int(128).bit_width(), Some(128));
|
||||||
|
assert_eq!(VarType::Bool.bit_width(), Some(1));
|
||||||
|
assert_eq!(VarType::Address.bit_width(), Some(160));
|
||||||
|
assert_eq!(VarType::Bytes(32).bit_width(), Some(256));
|
||||||
|
assert_eq!(VarType::String.bit_width(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_var_type_is_numeric() {
|
||||||
|
assert!(VarType::Uint(256).is_numeric());
|
||||||
|
assert!(VarType::Int(128).is_numeric());
|
||||||
|
assert!(!VarType::Bool.is_numeric());
|
||||||
|
assert!(!VarType::Address.is_numeric());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verification_status() {
|
||||||
|
let mut report = VerificationReport::new("TestContract");
|
||||||
|
assert_eq!(report.status, VerificationStatus::Unknown);
|
||||||
|
|
||||||
|
report.compute_status();
|
||||||
|
assert_eq!(report.status, VerificationStatus::Verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verifier_creation() {
|
||||||
|
let verifier = Verifier::new();
|
||||||
|
assert_eq!(verifier.config.timeout_ms, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
616
crates/synor-verifier/src/parser.rs
Normal file
616
crates/synor-verifier/src/parser.rs
Normal file
|
|
@ -0,0 +1,616 @@
|
||||||
|
//! Parser for the verification DSL.
|
||||||
|
//!
|
||||||
|
//! Parses specification files and Solidity-style annotations.
|
||||||
|
|
||||||
|
use crate::ast::*;
|
||||||
|
use crate::error::{VerifierError, VerifierResult};
|
||||||
|
use crate::{FunctionSig, Mutability, VarType, VerificationContext, Visibility};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Specification parser.
|
||||||
|
pub struct SpecParser;
|
||||||
|
|
||||||
|
impl SpecParser {
|
||||||
|
/// Parses a contract from source code with embedded specifications.
|
||||||
|
pub fn parse_contract(source: &str) -> VerifierResult<VerificationContext> {
|
||||||
|
let mut ctx = VerificationContext {
|
||||||
|
contract_name: String::new(),
|
||||||
|
source: source.to_string(),
|
||||||
|
specs: Vec::new(),
|
||||||
|
state_vars: HashMap::new(),
|
||||||
|
functions: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let lines: Vec<&str> = source.lines().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < lines.len() {
|
||||||
|
let line = lines[i].trim();
|
||||||
|
|
||||||
|
// Parse contract declaration
|
||||||
|
if line.starts_with("contract ") || line.starts_with("contract\t") {
|
||||||
|
ctx.contract_name = Self::parse_contract_name(line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse specification comments
|
||||||
|
if line.starts_with("/// @") || line.starts_with("// @") {
|
||||||
|
let spec = Self::parse_spec_comment(line, &lines, &mut i)?;
|
||||||
|
ctx.specs.push(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse state variable
|
||||||
|
if Self::is_state_var_line(line) {
|
||||||
|
let (name, var_type) = Self::parse_state_var(line)?;
|
||||||
|
ctx.state_vars.insert(name, var_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse function
|
||||||
|
if line.starts_with("function ") {
|
||||||
|
let func = Self::parse_function_sig(line)?;
|
||||||
|
ctx.functions.insert(func.name.clone(), func);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses contract name from declaration line.
|
||||||
|
fn parse_contract_name(line: &str) -> VerifierResult<String> {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
Ok(parts[1].trim_end_matches('{').to_string())
|
||||||
|
} else {
|
||||||
|
Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: "Invalid contract declaration".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a specification comment.
|
||||||
|
fn parse_spec_comment(
|
||||||
|
line: &str,
|
||||||
|
_lines: &[&str],
|
||||||
|
_i: &mut usize,
|
||||||
|
) -> VerifierResult<Specification> {
|
||||||
|
let content = line
|
||||||
|
.trim_start_matches("///")
|
||||||
|
.trim_start_matches("//")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if content.starts_with("@invariant") {
|
||||||
|
Self::parse_invariant(content)
|
||||||
|
} else if content.starts_with("@property") {
|
||||||
|
Self::parse_property(content)
|
||||||
|
} else if content.starts_with("@requires") || content.starts_with("@ensures") {
|
||||||
|
Self::parse_annotation(content)
|
||||||
|
} else {
|
||||||
|
Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: format!("Unknown specification: {}", content),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an invariant specification.
|
||||||
|
fn parse_invariant(content: &str) -> VerifierResult<Specification> {
|
||||||
|
let parts: Vec<&str> = content.splitn(2, ' ').collect();
|
||||||
|
let expr_str = if parts.len() > 1 { parts[1] } else { "true" };
|
||||||
|
|
||||||
|
// Extract name if provided: @invariant name: expr
|
||||||
|
let (name, expr_str) = if let Some(idx) = expr_str.find(':') {
|
||||||
|
let name = expr_str[..idx].trim().to_string();
|
||||||
|
let expr = expr_str[idx + 1..].trim();
|
||||||
|
(name, expr)
|
||||||
|
} else {
|
||||||
|
("unnamed".to_string(), expr_str)
|
||||||
|
};
|
||||||
|
|
||||||
|
let expr = Self::parse_expression(expr_str)?;
|
||||||
|
|
||||||
|
Ok(Specification::Invariant(Invariant {
|
||||||
|
name,
|
||||||
|
expr,
|
||||||
|
description: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a property specification.
|
||||||
|
fn parse_property(content: &str) -> VerifierResult<Specification> {
|
||||||
|
let parts: Vec<&str> = content.splitn(2, ' ').collect();
|
||||||
|
let expr_str = if parts.len() > 1 { parts[1] } else { "true" };
|
||||||
|
|
||||||
|
let (name, kind, expr_str) = Self::parse_property_header(expr_str)?;
|
||||||
|
let expr = Self::parse_expression(expr_str)?;
|
||||||
|
|
||||||
|
Ok(Specification::Property(Property {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
expr,
|
||||||
|
description: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses property header to extract name and kind.
|
||||||
|
fn parse_property_header(s: &str) -> VerifierResult<(String, PropertyKind, &str)> {
|
||||||
|
// Format: [safety|liveness] name: expr
|
||||||
|
let kind = if s.starts_with("safety") {
|
||||||
|
PropertyKind::Safety
|
||||||
|
} else if s.starts_with("liveness") {
|
||||||
|
PropertyKind::Liveness
|
||||||
|
} else if s.starts_with("reachability") {
|
||||||
|
PropertyKind::Reachability
|
||||||
|
} else {
|
||||||
|
PropertyKind::Custom
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip kind keyword
|
||||||
|
let s = s
|
||||||
|
.trim_start_matches("safety")
|
||||||
|
.trim_start_matches("liveness")
|
||||||
|
.trim_start_matches("reachability")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Extract name
|
||||||
|
if let Some(idx) = s.find(':') {
|
||||||
|
let name = s[..idx].trim().to_string();
|
||||||
|
let expr = s[idx + 1..].trim();
|
||||||
|
Ok((name, kind, expr))
|
||||||
|
} else {
|
||||||
|
Ok(("unnamed".to_string(), kind, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a function annotation.
|
||||||
|
fn parse_annotation(content: &str) -> VerifierResult<Specification> {
|
||||||
|
let (kind, rest) = if content.starts_with("@requires") {
|
||||||
|
("requires", content.trim_start_matches("@requires").trim())
|
||||||
|
} else {
|
||||||
|
("ensures", content.trim_start_matches("@ensures").trim())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expr = Self::parse_expression(rest)?;
|
||||||
|
|
||||||
|
let annotation = Annotation {
|
||||||
|
function: String::new(), // Will be filled in by context
|
||||||
|
requires: if kind == "requires" {
|
||||||
|
vec![expr.clone()]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
ensures: if kind == "ensures" {
|
||||||
|
vec![expr]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
modifies: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Specification::Annotation(annotation))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an expression from string.
|
||||||
|
pub fn parse_expression(s: &str) -> VerifierResult<Expression> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
// Handle parentheses
|
||||||
|
if s.starts_with('(') && s.ends_with(')') {
|
||||||
|
return Self::parse_expression(&s[1..s.len() - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle quantifiers
|
||||||
|
if s.starts_with("forall") || s.starts_with("exists") {
|
||||||
|
return Self::parse_quantifier(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle old()
|
||||||
|
if s.starts_with("old(") && s.ends_with(')') {
|
||||||
|
let inner = &s[4..s.len() - 1];
|
||||||
|
return Ok(Expression::Old(Box::new(Self::parse_expression(inner)?)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle binary operators (lowest precedence first)
|
||||||
|
for (op_str, op) in [
|
||||||
|
("==>", BinaryOperator::Implies),
|
||||||
|
("<==>", BinaryOperator::Iff),
|
||||||
|
("||", BinaryOperator::Or),
|
||||||
|
("&&", BinaryOperator::And),
|
||||||
|
("==", BinaryOperator::Eq),
|
||||||
|
("!=", BinaryOperator::Ne),
|
||||||
|
("<=", BinaryOperator::Le),
|
||||||
|
(">=", BinaryOperator::Ge),
|
||||||
|
("<", BinaryOperator::Lt),
|
||||||
|
(">", BinaryOperator::Gt),
|
||||||
|
("+", BinaryOperator::Add),
|
||||||
|
("-", BinaryOperator::Sub),
|
||||||
|
("*", BinaryOperator::Mul),
|
||||||
|
("/", BinaryOperator::Div),
|
||||||
|
("%", BinaryOperator::Mod),
|
||||||
|
] {
|
||||||
|
if let Some(idx) = Self::find_operator(s, op_str) {
|
||||||
|
let left = Self::parse_expression(&s[..idx])?;
|
||||||
|
let right = Self::parse_expression(&s[idx + op_str.len()..])?;
|
||||||
|
return Ok(Expression::BinaryOp {
|
||||||
|
left: Box::new(left),
|
||||||
|
op,
|
||||||
|
right: Box::new(right),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unary operators
|
||||||
|
if s.starts_with('!') {
|
||||||
|
let operand = Self::parse_expression(&s[1..])?;
|
||||||
|
return Ok(Expression::UnaryOp {
|
||||||
|
op: UnaryOperator::Not,
|
||||||
|
operand: Box::new(operand),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle literals
|
||||||
|
if s == "true" {
|
||||||
|
return Ok(Expression::Literal(Literal::Bool(true)));
|
||||||
|
}
|
||||||
|
if s == "false" {
|
||||||
|
return Ok(Expression::Literal(Literal::Bool(false)));
|
||||||
|
}
|
||||||
|
if let Ok(n) = s.parse::<u128>() {
|
||||||
|
return Ok(Expression::Literal(Literal::Uint(n)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special keywords
|
||||||
|
if s == "result" {
|
||||||
|
return Ok(Expression::Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle member access
|
||||||
|
if let Some(idx) = s.rfind('.') {
|
||||||
|
let object = Self::parse_expression(&s[..idx])?;
|
||||||
|
let member = s[idx + 1..].to_string();
|
||||||
|
return Ok(Expression::MemberAccess {
|
||||||
|
object: Box::new(object),
|
||||||
|
member,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array index
|
||||||
|
if s.ends_with(']') {
|
||||||
|
if let Some(idx) = s.rfind('[') {
|
||||||
|
let array = Self::parse_expression(&s[..idx])?;
|
||||||
|
let index = Self::parse_expression(&s[idx + 1..s.len() - 1])?;
|
||||||
|
return Ok(Expression::Index {
|
||||||
|
array: Box::new(array),
|
||||||
|
index: Box::new(index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle function calls
|
||||||
|
if s.ends_with(')') {
|
||||||
|
if let Some(idx) = s.find('(') {
|
||||||
|
let function = s[..idx].to_string();
|
||||||
|
let args_str = &s[idx + 1..s.len() - 1];
|
||||||
|
let args = Self::parse_args(args_str)?;
|
||||||
|
return Ok(Expression::FunctionCall { function, args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: variable reference
|
||||||
|
Ok(Expression::Variable(s.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds an operator in string, respecting parentheses.
|
||||||
|
fn find_operator(s: &str, op: &str) -> Option<usize> {
|
||||||
|
let mut depth = 0;
|
||||||
|
let chars: Vec<char> = s.chars().collect();
|
||||||
|
let op_chars: Vec<char> = op.chars().collect();
|
||||||
|
|
||||||
|
for i in 0..chars.len() {
|
||||||
|
match chars[i] {
|
||||||
|
'(' | '[' => depth += 1,
|
||||||
|
')' | ']' => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth == 0 && i + op_chars.len() <= chars.len() {
|
||||||
|
let slice: String = chars[i..i + op_chars.len()].iter().collect();
|
||||||
|
if slice == op {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a quantifier expression.
|
||||||
|
fn parse_quantifier(s: &str) -> VerifierResult<Expression> {
|
||||||
|
let kind = if s.starts_with("forall") {
|
||||||
|
QuantifierKind::ForAll
|
||||||
|
} else {
|
||||||
|
QuantifierKind::Exists
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format: forall x: uint256 :: expr
|
||||||
|
let rest = s
|
||||||
|
.trim_start_matches("forall")
|
||||||
|
.trim_start_matches("exists")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Find variable and type
|
||||||
|
let parts: Vec<&str> = rest.splitn(2, "::").collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: "Invalid quantifier syntax".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let var_part = parts[0].trim();
|
||||||
|
let body_str = parts[1].trim();
|
||||||
|
|
||||||
|
let (var, var_type) = Self::parse_typed_var(var_part)?;
|
||||||
|
let body = Self::parse_expression(body_str)?;
|
||||||
|
|
||||||
|
Ok(Expression::Quantifier {
|
||||||
|
kind,
|
||||||
|
var,
|
||||||
|
var_type,
|
||||||
|
body: Box::new(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a typed variable declaration.
|
||||||
|
fn parse_typed_var(s: &str) -> VerifierResult<(String, VarType)> {
|
||||||
|
let parts: Vec<&str> = s.split(':').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: "Invalid typed variable".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let var = parts[0].trim().to_string();
|
||||||
|
let type_str = parts[1].trim();
|
||||||
|
let var_type = Self::parse_type(type_str)?;
|
||||||
|
|
||||||
|
Ok((var, var_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a type string.
|
||||||
|
pub fn parse_type(s: &str) -> VerifierResult<VarType> {
|
||||||
|
let s = s.trim();
|
||||||
|
|
||||||
|
if s.starts_with("uint") {
|
||||||
|
let bits: u16 = s[4..].parse().unwrap_or(256);
|
||||||
|
return Ok(VarType::Uint(bits));
|
||||||
|
}
|
||||||
|
if s.starts_with("int") {
|
||||||
|
let bits: u16 = s[3..].parse().unwrap_or(256);
|
||||||
|
return Ok(VarType::Int(bits));
|
||||||
|
}
|
||||||
|
if s == "bool" {
|
||||||
|
return Ok(VarType::Bool);
|
||||||
|
}
|
||||||
|
if s == "address" {
|
||||||
|
return Ok(VarType::Address);
|
||||||
|
}
|
||||||
|
if s.starts_with("bytes") && s.len() > 5 {
|
||||||
|
let n: u16 = s[5..].parse().unwrap_or(32);
|
||||||
|
return Ok(VarType::Bytes(n));
|
||||||
|
}
|
||||||
|
if s == "bytes" {
|
||||||
|
return Ok(VarType::DynBytes);
|
||||||
|
}
|
||||||
|
if s == "string" {
|
||||||
|
return Ok(VarType::String);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array type
|
||||||
|
if s.ends_with("[]") {
|
||||||
|
let inner = Self::parse_type(&s[..s.len() - 2])?;
|
||||||
|
return Ok(VarType::Array(Box::new(inner)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping type
|
||||||
|
if s.starts_with("mapping(") && s.ends_with(')') {
|
||||||
|
let inner = &s[8..s.len() - 1];
|
||||||
|
if let Some(idx) = inner.find("=>") {
|
||||||
|
let key = Self::parse_type(&inner[..idx].trim())?;
|
||||||
|
let value = Self::parse_type(&inner[idx + 2..].trim())?;
|
||||||
|
return Ok(VarType::Mapping(Box::new(key), Box::new(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: format!("Unknown type: {}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses function arguments.
|
||||||
|
fn parse_args(s: &str) -> VerifierResult<Vec<Expression>> {
|
||||||
|
if s.trim().is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut args = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut depth = 0;
|
||||||
|
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'(' | '[' => {
|
||||||
|
depth += 1;
|
||||||
|
current.push(c);
|
||||||
|
}
|
||||||
|
')' | ']' => {
|
||||||
|
depth -= 1;
|
||||||
|
current.push(c);
|
||||||
|
}
|
||||||
|
',' if depth == 0 => {
|
||||||
|
args.push(Self::parse_expression(current.trim())?);
|
||||||
|
current.clear();
|
||||||
|
}
|
||||||
|
_ => current.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current.trim().is_empty() {
|
||||||
|
args.push(Self::parse_expression(current.trim())?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a line declares a state variable.
|
||||||
|
fn is_state_var_line(line: &str) -> bool {
|
||||||
|
// Simple heuristic: type followed by visibility/name
|
||||||
|
let keywords = ["uint", "int", "bool", "address", "bytes", "string", "mapping"];
|
||||||
|
keywords.iter().any(|k| line.starts_with(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a state variable declaration.
|
||||||
|
fn parse_state_var(line: &str) -> VerifierResult<(String, VarType)> {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return Err(VerifierError::ParseError {
|
||||||
|
line: 0,
|
||||||
|
message: "Invalid state variable".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let var_type = Self::parse_type(parts[0])?;
|
||||||
|
let name = parts
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.trim_end_matches(';')
|
||||||
|
.trim_end_matches('=')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((name, var_type))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a function signature.
|
||||||
|
fn parse_function_sig(line: &str) -> VerifierResult<FunctionSig> {
|
||||||
|
let line = line.trim_start_matches("function").trim();
|
||||||
|
|
||||||
|
// Extract function name
|
||||||
|
let name_end = line.find('(').unwrap_or(line.len());
|
||||||
|
let name = line[..name_end].trim().to_string();
|
||||||
|
|
||||||
|
// Extract parameters
|
||||||
|
let params_start = line.find('(').unwrap_or(0);
|
||||||
|
let params_end = line.find(')').unwrap_or(line.len());
|
||||||
|
let params_str = &line[params_start + 1..params_end];
|
||||||
|
let params = Self::parse_params(params_str)?;
|
||||||
|
|
||||||
|
// Extract return type
|
||||||
|
let returns = if let Some(idx) = line.find("returns") {
|
||||||
|
let ret_start = line[idx..].find('(').map(|i| idx + i + 1);
|
||||||
|
let ret_end = line[idx..].find(')').map(|i| idx + i);
|
||||||
|
if let (Some(start), Some(end)) = (ret_start, ret_end) {
|
||||||
|
Some(Self::parse_type(&line[start..end])?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract visibility and mutability
|
||||||
|
let visibility = if line.contains("external") {
|
||||||
|
Visibility::External
|
||||||
|
} else if line.contains("internal") {
|
||||||
|
Visibility::Internal
|
||||||
|
} else if line.contains("private") {
|
||||||
|
Visibility::Private
|
||||||
|
} else {
|
||||||
|
Visibility::Public
|
||||||
|
};
|
||||||
|
|
||||||
|
let mutability = if line.contains("pure") {
|
||||||
|
Mutability::Pure
|
||||||
|
} else if line.contains("view") {
|
||||||
|
Mutability::View
|
||||||
|
} else if line.contains("payable") {
|
||||||
|
Mutability::Payable
|
||||||
|
} else {
|
||||||
|
Mutability::Nonpayable
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(FunctionSig {
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
returns,
|
||||||
|
visibility,
|
||||||
|
mutability,
|
||||||
|
preconditions: vec![],
|
||||||
|
postconditions: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses function parameters.
|
||||||
|
fn parse_params(s: &str) -> VerifierResult<Vec<(String, VarType)>> {
|
||||||
|
if s.trim().is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut params = Vec::new();
|
||||||
|
for param in s.split(',') {
|
||||||
|
let parts: Vec<&str> = param.split_whitespace().collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let var_type = Self::parse_type(parts[0])?;
|
||||||
|
let name = parts.last().unwrap().to_string();
|
||||||
|
params.push((name, var_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_type() {
|
||||||
|
assert_eq!(SpecParser::parse_type("uint256").unwrap(), VarType::Uint(256));
|
||||||
|
assert_eq!(SpecParser::parse_type("int128").unwrap(), VarType::Int(128));
|
||||||
|
assert_eq!(SpecParser::parse_type("bool").unwrap(), VarType::Bool);
|
||||||
|
assert_eq!(SpecParser::parse_type("address").unwrap(), VarType::Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_expression() {
|
||||||
|
let expr = SpecParser::parse_expression("x + 1").unwrap();
|
||||||
|
if let Expression::BinaryOp { op, .. } = expr {
|
||||||
|
assert_eq!(op, BinaryOperator::Add);
|
||||||
|
} else {
|
||||||
|
panic!("Expected BinaryOp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_comparison() {
|
||||||
|
let expr = SpecParser::parse_expression("balance >= 0").unwrap();
|
||||||
|
if let Expression::BinaryOp { op, .. } = expr {
|
||||||
|
assert_eq!(op, BinaryOperator::Ge);
|
||||||
|
} else {
|
||||||
|
panic!("Expected BinaryOp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_invariant() {
|
||||||
|
let spec = SpecParser::parse_invariant("@invariant positive: balance >= 0").unwrap();
|
||||||
|
if let Specification::Invariant(inv) = spec {
|
||||||
|
assert_eq!(inv.name, "positive");
|
||||||
|
} else {
|
||||||
|
panic!("Expected Invariant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
crates/synor-verifier/src/prover.rs
Normal file
308
crates/synor-verifier/src/prover.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
//! Proof generation and verification.
|
||||||
|
//!
|
||||||
|
//! Integrates with SMT solvers to prove or disprove properties.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::ast::Expression;
|
||||||
|
use crate::error::{VerifierError, VerifierResult};
|
||||||
|
use crate::smt::{SmtContext, SmtResult};
|
||||||
|
use crate::symbolic::SymbolicState;
|
||||||
|
|
||||||
|
/// Prover configuration.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ProverConfig {
|
||||||
|
/// Timeout in milliseconds.
|
||||||
|
pub timeout_ms: u64,
|
||||||
|
/// Maximum symbolic execution depth.
|
||||||
|
pub max_depth: usize,
|
||||||
|
/// Maximum loop unrolling iterations.
|
||||||
|
pub max_loop_unroll: usize,
|
||||||
|
/// Enable counterexample generation.
|
||||||
|
pub generate_counterexamples: bool,
|
||||||
|
/// Enable proof caching.
|
||||||
|
pub enable_caching: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProverConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
timeout_ms: 30000,
|
||||||
|
max_depth: 100,
|
||||||
|
max_loop_unroll: 10,
|
||||||
|
generate_counterexamples: true,
|
||||||
|
enable_caching: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a proof attempt.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ProofResult {
|
||||||
|
/// Property is proven to hold.
|
||||||
|
Proven {
|
||||||
|
/// Time taken in milliseconds.
|
||||||
|
time_ms: u64,
|
||||||
|
},
|
||||||
|
/// Property is disproven with counterexample.
|
||||||
|
Disproven {
|
||||||
|
/// Counterexample assignments.
|
||||||
|
counterexample: Counterexample,
|
||||||
|
/// Time taken.
|
||||||
|
time_ms: u64,
|
||||||
|
},
|
||||||
|
/// Could not determine (timeout, resource limit).
|
||||||
|
Unknown {
|
||||||
|
/// Reason for unknown result.
|
||||||
|
reason: String,
|
||||||
|
/// Time taken.
|
||||||
|
time_ms: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProofResult {
|
||||||
|
/// Returns true if property is proven.
|
||||||
|
pub fn is_proven(&self) -> bool {
|
||||||
|
matches!(self, ProofResult::Proven { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the counterexample if disproven.
|
||||||
|
pub fn counterexample(&self) -> Option<&Counterexample> {
|
||||||
|
if let ProofResult::Disproven { counterexample, .. } = self {
|
||||||
|
Some(counterexample)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counterexample showing how property can fail.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Counterexample {
|
||||||
|
/// Variable assignments that cause failure.
|
||||||
|
pub assignments: Vec<(String, String)>,
|
||||||
|
/// Execution trace leading to failure.
|
||||||
|
pub trace: Vec<String>,
|
||||||
|
/// The failing property.
|
||||||
|
pub property: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counterexample {
|
||||||
|
/// Creates a new counterexample.
|
||||||
|
pub fn new(property: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
assignments: Vec::new(),
|
||||||
|
trace: Vec::new(),
|
||||||
|
property: property.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a variable assignment.
|
||||||
|
pub fn add_assignment(&mut self, var: &str, value: &str) {
|
||||||
|
self.assignments.push((var.to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a trace step.
|
||||||
|
pub fn add_trace(&mut self, step: &str) {
|
||||||
|
self.trace.push(step.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the counterexample for display.
|
||||||
|
pub fn format(&self) -> String {
|
||||||
|
let mut s = format!("Counterexample for '{}':\n", self.property);
|
||||||
|
s.push_str(" Assignments:\n");
|
||||||
|
for (var, val) in &self.assignments {
|
||||||
|
s.push_str(&format!(" {} = {}\n", var, val));
|
||||||
|
}
|
||||||
|
if !self.trace.is_empty() {
|
||||||
|
s.push_str(" Trace:\n");
|
||||||
|
for (i, step) in self.trace.iter().enumerate() {
|
||||||
|
s.push_str(&format!(" {}. {}\n", i + 1, step));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main prover interface.
|
||||||
|
pub struct Prover {
|
||||||
|
config: ProverConfig,
|
||||||
|
smt: SmtContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Prover {
|
||||||
|
/// Creates a new prover with configuration.
|
||||||
|
pub fn new(config: ProverConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
smt: SmtContext::new(config.timeout_ms),
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proves or disproves an expression holds in all states.
|
||||||
|
pub fn prove(&self, expr: &Expression, state: &SymbolicState) -> VerifierResult<ProofResult> {
|
||||||
|
let start = Instant::now();
|
||||||
|
let timeout = Duration::from_millis(self.config.timeout_ms);
|
||||||
|
|
||||||
|
// Convert expression and state to SMT constraints
|
||||||
|
let constraints = self.smt.encode_state(state)?;
|
||||||
|
let property = self.smt.encode_expression(expr)?;
|
||||||
|
|
||||||
|
// Check if negation is unsatisfiable (property always holds)
|
||||||
|
let negated = self.smt.negate(&property)?;
|
||||||
|
|
||||||
|
let result = self.smt.check_sat_with_constraints(&constraints, &negated)?;
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
if elapsed > self.config.timeout_ms {
|
||||||
|
return Ok(ProofResult::Unknown {
|
||||||
|
reason: "Timeout".to_string(),
|
||||||
|
time_ms: elapsed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match result {
|
||||||
|
SmtResult::Unsat => {
|
||||||
|
// Negation is unsat, so property always holds
|
||||||
|
Ok(ProofResult::Proven { time_ms: elapsed })
|
||||||
|
}
|
||||||
|
SmtResult::Sat(model) => {
|
||||||
|
// Found counterexample
|
||||||
|
let counterexample = if self.config.generate_counterexamples {
|
||||||
|
self.extract_counterexample(&model, expr)
|
||||||
|
} else {
|
||||||
|
Counterexample::new("(counterexample generation disabled)")
|
||||||
|
};
|
||||||
|
Ok(ProofResult::Disproven {
|
||||||
|
counterexample,
|
||||||
|
time_ms: elapsed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SmtResult::Unknown(reason) => Ok(ProofResult::Unknown {
|
||||||
|
reason,
|
||||||
|
time_ms: elapsed,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proves an implication: precondition => postcondition.
|
||||||
|
pub fn prove_implication(
|
||||||
|
&self,
|
||||||
|
precondition: &Expression,
|
||||||
|
postcondition: &Expression,
|
||||||
|
state: &SymbolicState,
|
||||||
|
) -> VerifierResult<ProofResult> {
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// Encode: precondition AND NOT postcondition
|
||||||
|
let pre = self.smt.encode_expression(precondition)?;
|
||||||
|
let post = self.smt.encode_expression(postcondition)?;
|
||||||
|
let neg_post = self.smt.negate(&post)?;
|
||||||
|
|
||||||
|
let constraints = self.smt.encode_state(state)?;
|
||||||
|
|
||||||
|
// If pre AND NOT post is unsat, then pre => post
|
||||||
|
let combined = self.smt.and(&pre, &neg_post)?;
|
||||||
|
let result = self.smt.check_sat_with_constraints(&constraints, &combined)?;
|
||||||
|
|
||||||
|
let elapsed = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
SmtResult::Unsat => Ok(ProofResult::Proven { time_ms: elapsed }),
|
||||||
|
SmtResult::Sat(model) => {
|
||||||
|
let counterexample = self.extract_counterexample(&model, postcondition);
|
||||||
|
Ok(ProofResult::Disproven {
|
||||||
|
counterexample,
|
||||||
|
time_ms: elapsed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SmtResult::Unknown(reason) => Ok(ProofResult::Unknown {
|
||||||
|
reason,
|
||||||
|
time_ms: elapsed,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts a counterexample from an SMT model.
|
||||||
|
fn extract_counterexample(
|
||||||
|
&self,
|
||||||
|
model: &[(String, String)],
|
||||||
|
expr: &Expression,
|
||||||
|
) -> Counterexample {
|
||||||
|
let mut ce = Counterexample::new(&format!("{:?}", expr));
|
||||||
|
|
||||||
|
for (var, val) in model {
|
||||||
|
ce.add_assignment(var, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
ce
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an expression is satisfiable.
|
||||||
|
pub fn check_sat(&self, expr: &Expression) -> VerifierResult<bool> {
|
||||||
|
let encoded = self.smt.encode_expression(expr)?;
|
||||||
|
let result = self.smt.check_sat(&encoded)?;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
SmtResult::Sat(_) => Ok(true),
|
||||||
|
SmtResult::Unsat => Ok(false),
|
||||||
|
SmtResult::Unknown(reason) => Err(VerifierError::SmtError(reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simplifies an expression using the SMT solver.
|
||||||
|
pub fn simplify(&self, expr: &Expression) -> VerifierResult<Expression> {
|
||||||
|
// For now, just return the original expression
|
||||||
|
// Full implementation would use SMT solver's simplify
|
||||||
|
Ok(expr.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ast::{BinaryOperator, Literal};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prover_config_default() {
|
||||||
|
let config = ProverConfig::default();
|
||||||
|
assert_eq!(config.timeout_ms, 30000);
|
||||||
|
assert_eq!(config.max_depth, 100);
|
||||||
|
assert!(config.generate_counterexamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_counterexample_format() {
|
||||||
|
let mut ce = Counterexample::new("x > 0");
|
||||||
|
ce.add_assignment("x", "-1");
|
||||||
|
ce.add_trace("Entered function foo");
|
||||||
|
ce.add_trace("x assigned -1");
|
||||||
|
|
||||||
|
let formatted = ce.format();
|
||||||
|
assert!(formatted.contains("x = -1"));
|
||||||
|
assert!(formatted.contains("Entered function foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proof_result() {
|
||||||
|
let proven = ProofResult::Proven { time_ms: 100 };
|
||||||
|
assert!(proven.is_proven());
|
||||||
|
assert!(proven.counterexample().is_none());
|
||||||
|
|
||||||
|
let ce = Counterexample::new("test");
|
||||||
|
let disproven = ProofResult::Disproven {
|
||||||
|
counterexample: ce,
|
||||||
|
time_ms: 50,
|
||||||
|
};
|
||||||
|
assert!(!disproven.is_proven());
|
||||||
|
assert!(disproven.counterexample().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prover_creation() {
|
||||||
|
let prover = Prover::new(ProverConfig::default());
|
||||||
|
assert_eq!(prover.config.timeout_ms, 30000);
|
||||||
|
}
|
||||||
|
}
|
||||||
418
crates/synor-verifier/src/smt.rs
Normal file
418
crates/synor-verifier/src/smt.rs
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
//! SMT solver integration.
|
||||||
|
//!
|
||||||
|
//! Provides an interface to SMT solvers for constraint solving.
|
||||||
|
|
||||||
|
use crate::ast::{BinaryOperator, Expression, Literal, QuantifierKind, UnaryOperator};
|
||||||
|
use crate::error::{VerifierError, VerifierResult};
|
||||||
|
use crate::symbolic::{SymbolicState, SymbolicValue};
|
||||||
|
use crate::VarType;
|
||||||
|
|
||||||
|
/// SMT solver result.
|
||||||
|
pub enum SmtResult {
|
||||||
|
/// Formula is satisfiable with model.
|
||||||
|
Sat(Vec<(String, String)>),
|
||||||
|
/// Formula is unsatisfiable.
|
||||||
|
Unsat,
|
||||||
|
/// Unknown (timeout, etc.).
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encoded SMT expression (string representation).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SmtExpr(String);
|
||||||
|
|
||||||
|
impl SmtExpr {
|
||||||
|
pub fn new(s: &str) -> Self {
|
||||||
|
SmtExpr(s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMT solver context.
|
||||||
|
pub struct SmtContext {
|
||||||
|
/// Timeout in milliseconds.
|
||||||
|
timeout_ms: u64,
|
||||||
|
/// Variable declarations.
|
||||||
|
declarations: Vec<String>,
|
||||||
|
/// Assertions.
|
||||||
|
assertions: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtContext {
|
||||||
|
/// Creates a new SMT context.
|
||||||
|
pub fn new(timeout_ms: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
timeout_ms,
|
||||||
|
declarations: Vec::new(),
|
||||||
|
assertions: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a symbolic state as SMT constraints.
|
||||||
|
pub fn encode_state(&self, state: &SymbolicState) -> VerifierResult<Vec<SmtExpr>> {
|
||||||
|
let mut constraints = Vec::new();
|
||||||
|
|
||||||
|
// Encode path condition
|
||||||
|
for cond in state.path_condition() {
|
||||||
|
let encoded = self.encode_symbolic_value(cond)?;
|
||||||
|
constraints.push(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes an AST expression as SMT.
|
||||||
|
pub fn encode_expression(&self, expr: &Expression) -> VerifierResult<SmtExpr> {
|
||||||
|
let smt_str = self.expr_to_smt(expr)?;
|
||||||
|
Ok(SmtExpr::new(&smt_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an AST expression to SMT-LIB format.
|
||||||
|
fn expr_to_smt(&self, expr: &Expression) -> VerifierResult<String> {
|
||||||
|
match expr {
|
||||||
|
Expression::Literal(lit) => self.literal_to_smt(lit),
|
||||||
|
|
||||||
|
Expression::Variable(name) => Ok(self.sanitize_name(name)),
|
||||||
|
|
||||||
|
Expression::BinaryOp { left, op, right } => {
|
||||||
|
let l = self.expr_to_smt(left)?;
|
||||||
|
let r = self.expr_to_smt(right)?;
|
||||||
|
let op_str = self.binop_to_smt(*op);
|
||||||
|
Ok(format!("({} {} {})", op_str, l, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::UnaryOp { op, operand } => {
|
||||||
|
let v = self.expr_to_smt(operand)?;
|
||||||
|
let op_str = self.unop_to_smt(*op);
|
||||||
|
Ok(format!("({} {})", op_str, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Ternary {
|
||||||
|
condition,
|
||||||
|
then_expr,
|
||||||
|
else_expr,
|
||||||
|
} => {
|
||||||
|
let c = self.expr_to_smt(condition)?;
|
||||||
|
let t = self.expr_to_smt(then_expr)?;
|
||||||
|
let e = self.expr_to_smt(else_expr)?;
|
||||||
|
Ok(format!("(ite {} {} {})", c, t, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Quantifier {
|
||||||
|
kind,
|
||||||
|
var,
|
||||||
|
var_type,
|
||||||
|
body,
|
||||||
|
} => {
|
||||||
|
let sort = self.type_to_smt(var_type);
|
||||||
|
let body_smt = self.expr_to_smt(body)?;
|
||||||
|
let quantifier = match kind {
|
||||||
|
QuantifierKind::ForAll => "forall",
|
||||||
|
QuantifierKind::Exists => "exists",
|
||||||
|
};
|
||||||
|
Ok(format!(
|
||||||
|
"({} (({} {})) {})",
|
||||||
|
quantifier,
|
||||||
|
self.sanitize_name(var),
|
||||||
|
sort,
|
||||||
|
body_smt
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::FunctionCall { function, args } => {
|
||||||
|
let args_smt: Result<Vec<_>, _> =
|
||||||
|
args.iter().map(|a| self.expr_to_smt(a)).collect();
|
||||||
|
let args_str = args_smt?.join(" ");
|
||||||
|
Ok(format!("({} {})", self.sanitize_name(function), args_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::MemberAccess { object, member } => {
|
||||||
|
let obj = self.expr_to_smt(object)?;
|
||||||
|
Ok(format!("{}_{}", obj, self.sanitize_name(member)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Index { array, index } => {
|
||||||
|
let arr = self.expr_to_smt(array)?;
|
||||||
|
let idx = self.expr_to_smt(index)?;
|
||||||
|
Ok(format!("(select {} {})", arr, idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Old(inner) => {
|
||||||
|
let inner_smt = self.expr_to_smt(inner)?;
|
||||||
|
Ok(format!("{}_old", inner_smt))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Result => Ok("__result".to_string()),
|
||||||
|
|
||||||
|
Expression::Sum { var, range: _, body } => {
|
||||||
|
// Sum is modeled as uninterpreted function
|
||||||
|
let body_smt = self.expr_to_smt(body)?;
|
||||||
|
Ok(format!("(sum_{} {})", self.sanitize_name(var), body_smt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a literal to SMT-LIB format.
|
||||||
|
fn literal_to_smt(&self, lit: &Literal) -> VerifierResult<String> {
|
||||||
|
match lit {
|
||||||
|
Literal::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
|
||||||
|
Literal::Uint(n) => Ok(format!("(_ bv{} 256)", n)),
|
||||||
|
Literal::Int(n) => {
|
||||||
|
if *n >= 0 {
|
||||||
|
Ok(format!("(_ bv{} 256)", n))
|
||||||
|
} else {
|
||||||
|
Ok(format!("(bvneg (_ bv{} 256))", -n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Literal::Address(bytes) => {
|
||||||
|
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
|
||||||
|
Ok(format!("#x{}", hex))
|
||||||
|
}
|
||||||
|
Literal::Bytes(bytes) => {
|
||||||
|
let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
|
||||||
|
Ok(format!("#x{}", hex))
|
||||||
|
}
|
||||||
|
Literal::String(s) => Ok(format!("\"{}\"", s.replace('\"', "\"\""))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a binary operator to SMT-LIB.
|
||||||
|
fn binop_to_smt(&self, op: BinaryOperator) -> &'static str {
|
||||||
|
match op {
|
||||||
|
BinaryOperator::Add => "bvadd",
|
||||||
|
BinaryOperator::Sub => "bvsub",
|
||||||
|
BinaryOperator::Mul => "bvmul",
|
||||||
|
BinaryOperator::Div => "bvudiv",
|
||||||
|
BinaryOperator::Mod => "bvurem",
|
||||||
|
BinaryOperator::Exp => "bvexp", // Would need custom function
|
||||||
|
BinaryOperator::Eq => "=",
|
||||||
|
BinaryOperator::Ne => "distinct",
|
||||||
|
BinaryOperator::Lt => "bvult",
|
||||||
|
BinaryOperator::Le => "bvule",
|
||||||
|
BinaryOperator::Gt => "bvugt",
|
||||||
|
BinaryOperator::Ge => "bvuge",
|
||||||
|
BinaryOperator::And => "and",
|
||||||
|
BinaryOperator::Or => "or",
|
||||||
|
BinaryOperator::Implies => "=>",
|
||||||
|
BinaryOperator::Iff => "=",
|
||||||
|
BinaryOperator::BitAnd => "bvand",
|
||||||
|
BinaryOperator::BitOr => "bvor",
|
||||||
|
BinaryOperator::BitXor => "bvxor",
|
||||||
|
BinaryOperator::Shl => "bvshl",
|
||||||
|
BinaryOperator::Shr => "bvlshr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a unary operator to SMT-LIB.
|
||||||
|
fn unop_to_smt(&self, op: UnaryOperator) -> &'static str {
|
||||||
|
match op {
|
||||||
|
UnaryOperator::Not => "not",
|
||||||
|
UnaryOperator::Neg => "bvneg",
|
||||||
|
UnaryOperator::BitNot => "bvnot",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a type to SMT sort.
|
||||||
|
fn type_to_smt(&self, var_type: &VarType) -> String {
|
||||||
|
match var_type {
|
||||||
|
VarType::Uint(bits) => format!("(_ BitVec {})", bits),
|
||||||
|
VarType::Int(bits) => format!("(_ BitVec {})", bits),
|
||||||
|
VarType::Bool => "Bool".to_string(),
|
||||||
|
VarType::Address => "(_ BitVec 160)".to_string(),
|
||||||
|
VarType::Bytes(n) => format!("(_ BitVec {})", n * 8),
|
||||||
|
VarType::DynBytes => "(Array Int (_ BitVec 8))".to_string(),
|
||||||
|
VarType::String => "String".to_string(),
|
||||||
|
VarType::Array(inner) => {
|
||||||
|
format!("(Array Int {})", self.type_to_smt(inner))
|
||||||
|
}
|
||||||
|
VarType::Mapping(key, val) => {
|
||||||
|
format!(
|
||||||
|
"(Array {} {})",
|
||||||
|
self.type_to_smt(key),
|
||||||
|
self.type_to_smt(val)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VarType::Struct(name, _) => name.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitizes a name for SMT-LIB (removes invalid characters).
|
||||||
|
fn sanitize_name(&self, name: &str) -> String {
|
||||||
|
name.replace('.', "_")
|
||||||
|
.replace('[', "_")
|
||||||
|
.replace(']', "_")
|
||||||
|
.replace('(', "_")
|
||||||
|
.replace(')', "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a symbolic value as SMT.
|
||||||
|
fn encode_symbolic_value(&self, value: &SymbolicValue) -> VerifierResult<SmtExpr> {
|
||||||
|
let smt_str = self.symbolic_to_smt(value)?;
|
||||||
|
Ok(SmtExpr::new(&smt_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a symbolic value to SMT-LIB format.
|
||||||
|
fn symbolic_to_smt(&self, value: &SymbolicValue) -> VerifierResult<String> {
|
||||||
|
match value {
|
||||||
|
SymbolicValue::Concrete(lit) => self.literal_to_smt(lit),
|
||||||
|
|
||||||
|
SymbolicValue::Symbol(name, _) => Ok(self.sanitize_name(name)),
|
||||||
|
|
||||||
|
SymbolicValue::BinaryOp { left, op, right } => {
|
||||||
|
let l = self.symbolic_to_smt(left)?;
|
||||||
|
let r = self.symbolic_to_smt(right)?;
|
||||||
|
let op_str = self.binop_to_smt(*op);
|
||||||
|
Ok(format!("({} {} {})", op_str, l, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolicValue::UnaryOp { op, operand } => {
|
||||||
|
let v = self.symbolic_to_smt(operand)?;
|
||||||
|
let op_str = self.unop_to_smt(*op);
|
||||||
|
Ok(format!("({} {})", op_str, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolicValue::Ite {
|
||||||
|
condition,
|
||||||
|
then_val,
|
||||||
|
else_val,
|
||||||
|
} => {
|
||||||
|
let c = self.symbolic_to_smt(condition)?;
|
||||||
|
let t = self.symbolic_to_smt(then_val)?;
|
||||||
|
let e = self.symbolic_to_smt(else_val)?;
|
||||||
|
Ok(format!("(ite {} {} {})", c, t, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolicValue::Select { array, index } => {
|
||||||
|
let arr = self.symbolic_to_smt(array)?;
|
||||||
|
let idx = self.symbolic_to_smt(index)?;
|
||||||
|
Ok(format!("(select {} {})", arr, idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
SymbolicValue::Store {
|
||||||
|
array,
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
} => {
|
||||||
|
let arr = self.symbolic_to_smt(array)?;
|
||||||
|
let idx = self.symbolic_to_smt(index)?;
|
||||||
|
let val = self.symbolic_to_smt(value)?;
|
||||||
|
Ok(format!("(store {} {} {})", arr, idx, val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Negates an SMT expression.
|
||||||
|
pub fn negate(&self, expr: &SmtExpr) -> VerifierResult<SmtExpr> {
|
||||||
|
Ok(SmtExpr::new(&format!("(not {})", expr.as_str())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates conjunction of expressions.
|
||||||
|
pub fn and(&self, a: &SmtExpr, b: &SmtExpr) -> VerifierResult<SmtExpr> {
|
||||||
|
Ok(SmtExpr::new(&format!("(and {} {})", a.as_str(), b.as_str())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates disjunction of expressions.
|
||||||
|
pub fn or(&self, a: &SmtExpr, b: &SmtExpr) -> VerifierResult<SmtExpr> {
|
||||||
|
Ok(SmtExpr::new(&format!("(or {} {})", a.as_str(), b.as_str())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks satisfiability of an expression.
|
||||||
|
pub fn check_sat(&self, expr: &SmtExpr) -> VerifierResult<SmtResult> {
|
||||||
|
// Simulated SMT solving (real implementation would use Z3 or similar)
|
||||||
|
// For now, return Unknown for complex expressions, or evaluate simple ones
|
||||||
|
|
||||||
|
let s = expr.as_str();
|
||||||
|
|
||||||
|
// Simple cases
|
||||||
|
if s == "true" {
|
||||||
|
return Ok(SmtResult::Sat(vec![]));
|
||||||
|
}
|
||||||
|
if s == "false" {
|
||||||
|
return Ok(SmtResult::Unsat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown for complex expressions
|
||||||
|
Ok(SmtResult::Unknown(
|
||||||
|
"SMT backend not configured (enable z3-backend feature)".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks satisfiability with additional constraints.
|
||||||
|
pub fn check_sat_with_constraints(
|
||||||
|
&self,
|
||||||
|
constraints: &[SmtExpr],
|
||||||
|
expr: &SmtExpr,
|
||||||
|
) -> VerifierResult<SmtResult> {
|
||||||
|
// Combine all constraints
|
||||||
|
let mut combined = expr.clone();
|
||||||
|
for c in constraints {
|
||||||
|
combined = self.and(&combined, c)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.check_sat(&combined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_smt_context_creation() {
|
||||||
|
let ctx = SmtContext::new(30000);
|
||||||
|
assert_eq!(ctx.timeout_ms, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_literal_encoding() {
|
||||||
|
let ctx = SmtContext::new(1000);
|
||||||
|
|
||||||
|
assert_eq!(ctx.literal_to_smt(&Literal::Bool(true)).unwrap(), "true");
|
||||||
|
assert_eq!(ctx.literal_to_smt(&Literal::Bool(false)).unwrap(), "false");
|
||||||
|
assert_eq!(
|
||||||
|
ctx.literal_to_smt(&Literal::Uint(42)).unwrap(),
|
||||||
|
"(_ bv42 256)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_type_encoding() {
|
||||||
|
let ctx = SmtContext::new(1000);
|
||||||
|
|
||||||
|
assert_eq!(ctx.type_to_smt(&VarType::Bool), "Bool");
|
||||||
|
assert_eq!(ctx.type_to_smt(&VarType::Uint(256)), "(_ BitVec 256)");
|
||||||
|
assert_eq!(ctx.type_to_smt(&VarType::Address), "(_ BitVec 160)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_negate() {
|
||||||
|
let ctx = SmtContext::new(1000);
|
||||||
|
let expr = SmtExpr::new("x");
|
||||||
|
let neg = ctx.negate(&expr).unwrap();
|
||||||
|
assert_eq!(neg.as_str(), "(not x)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_and_or() {
|
||||||
|
let ctx = SmtContext::new(1000);
|
||||||
|
let a = SmtExpr::new("a");
|
||||||
|
let b = SmtExpr::new("b");
|
||||||
|
|
||||||
|
assert_eq!(ctx.and(&a, &b).unwrap().as_str(), "(and a b)");
|
||||||
|
assert_eq!(ctx.or(&a, &b).unwrap().as_str(), "(or a b)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_check_sat_simple() {
|
||||||
|
let ctx = SmtContext::new(1000);
|
||||||
|
|
||||||
|
let t = SmtExpr::new("true");
|
||||||
|
assert!(matches!(ctx.check_sat(&t).unwrap(), SmtResult::Sat(_)));
|
||||||
|
|
||||||
|
let f = SmtExpr::new("false");
|
||||||
|
assert!(matches!(ctx.check_sat(&f).unwrap(), SmtResult::Unsat));
|
||||||
|
}
|
||||||
|
}
|
||||||
561
crates/synor-verifier/src/symbolic.rs
Normal file
561
crates/synor-verifier/src/symbolic.rs
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
//! Symbolic execution engine.
|
||||||
|
//!
|
||||||
|
//! Explores all possible execution paths through a contract.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::ast::{BinaryOperator, Expression, Literal, Statement, UnaryOperator};
|
||||||
|
use crate::error::{VerifierError, VerifierResult};
|
||||||
|
use crate::prover::ProverConfig;
|
||||||
|
use crate::{VarType, VerificationContext};
|
||||||
|
|
||||||
|
/// Symbolic value representing an unknown or computed value.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum SymbolicValue {
|
||||||
|
/// Concrete literal value.
|
||||||
|
Concrete(Literal),
|
||||||
|
/// Symbolic variable.
|
||||||
|
Symbol(String, VarType),
|
||||||
|
/// Binary operation on symbolic values.
|
||||||
|
BinaryOp {
|
||||||
|
left: Box<SymbolicValue>,
|
||||||
|
op: BinaryOperator,
|
||||||
|
right: Box<SymbolicValue>,
|
||||||
|
},
|
||||||
|
/// Unary operation.
|
||||||
|
UnaryOp {
|
||||||
|
op: UnaryOperator,
|
||||||
|
operand: Box<SymbolicValue>,
|
||||||
|
},
|
||||||
|
/// Conditional value (ITE = if-then-else).
|
||||||
|
Ite {
|
||||||
|
condition: Box<SymbolicValue>,
|
||||||
|
then_val: Box<SymbolicValue>,
|
||||||
|
else_val: Box<SymbolicValue>,
|
||||||
|
},
|
||||||
|
/// Array/mapping read.
|
||||||
|
Select {
|
||||||
|
array: Box<SymbolicValue>,
|
||||||
|
index: Box<SymbolicValue>,
|
||||||
|
},
|
||||||
|
/// Array/mapping write.
|
||||||
|
Store {
|
||||||
|
array: Box<SymbolicValue>,
|
||||||
|
index: Box<SymbolicValue>,
|
||||||
|
value: Box<SymbolicValue>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolicValue {
|
||||||
|
/// Creates a new symbolic variable.
|
||||||
|
pub fn symbol(name: &str, var_type: VarType) -> Self {
|
||||||
|
SymbolicValue::Symbol(name.to_string(), var_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a concrete boolean value.
|
||||||
|
pub fn bool_const(v: bool) -> Self {
|
||||||
|
SymbolicValue::Concrete(Literal::Bool(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a concrete uint value.
|
||||||
|
pub fn uint_const(v: u128) -> Self {
|
||||||
|
SymbolicValue::Concrete(Literal::Uint(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a binary operation.
|
||||||
|
pub fn binary(left: SymbolicValue, op: BinaryOperator, right: SymbolicValue) -> Self {
|
||||||
|
SymbolicValue::BinaryOp {
|
||||||
|
left: Box::new(left),
|
||||||
|
op,
|
||||||
|
right: Box::new(right),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a conditional value.
|
||||||
|
pub fn ite(condition: SymbolicValue, then_val: SymbolicValue, else_val: SymbolicValue) -> Self {
|
||||||
|
SymbolicValue::Ite {
|
||||||
|
condition: Box::new(condition),
|
||||||
|
then_val: Box::new(then_val),
|
||||||
|
else_val: Box::new(else_val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to evaluate to a concrete value.
|
||||||
|
pub fn try_evaluate(&self) -> Option<Literal> {
|
||||||
|
match self {
|
||||||
|
SymbolicValue::Concrete(lit) => Some(lit.clone()),
|
||||||
|
SymbolicValue::BinaryOp { left, op, right } => {
|
||||||
|
let l = left.try_evaluate()?;
|
||||||
|
let r = right.try_evaluate()?;
|
||||||
|
Self::evaluate_binary(&l, *op, &r)
|
||||||
|
}
|
||||||
|
SymbolicValue::UnaryOp { op, operand } => {
|
||||||
|
let v = operand.try_evaluate()?;
|
||||||
|
Self::evaluate_unary(*op, &v)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_binary(left: &Literal, op: BinaryOperator, right: &Literal) -> Option<Literal> {
|
||||||
|
match (left, op, right) {
|
||||||
|
(Literal::Uint(a), BinaryOperator::Add, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Uint(a.wrapping_add(*b)))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Sub, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Uint(a.wrapping_sub(*b)))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Mul, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Uint(a.wrapping_mul(*b)))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Div, Literal::Uint(b)) if *b != 0 => {
|
||||||
|
Some(Literal::Uint(a / b))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Eq, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Bool(a == b))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Lt, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Bool(a < b))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Le, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Bool(a <= b))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Gt, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Bool(a > b))
|
||||||
|
}
|
||||||
|
(Literal::Uint(a), BinaryOperator::Ge, Literal::Uint(b)) => {
|
||||||
|
Some(Literal::Bool(a >= b))
|
||||||
|
}
|
||||||
|
(Literal::Bool(a), BinaryOperator::And, Literal::Bool(b)) => {
|
||||||
|
Some(Literal::Bool(*a && *b))
|
||||||
|
}
|
||||||
|
(Literal::Bool(a), BinaryOperator::Or, Literal::Bool(b)) => {
|
||||||
|
Some(Literal::Bool(*a || *b))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_unary(op: UnaryOperator, val: &Literal) -> Option<Literal> {
|
||||||
|
match (op, val) {
|
||||||
|
(UnaryOperator::Not, Literal::Bool(b)) => Some(Literal::Bool(!b)),
|
||||||
|
(UnaryOperator::Neg, Literal::Int(i)) => Some(Literal::Int(-i)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Symbolic execution state.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SymbolicState {
|
||||||
|
/// Variable bindings.
|
||||||
|
variables: HashMap<String, SymbolicValue>,
|
||||||
|
/// Path condition (constraints on this path).
|
||||||
|
path_condition: Vec<SymbolicValue>,
|
||||||
|
/// Execution depth.
|
||||||
|
depth: usize,
|
||||||
|
/// Whether this path is still feasible.
|
||||||
|
feasible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolicState {
|
||||||
|
/// Creates a new empty symbolic state.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
variables: HashMap::new(),
|
||||||
|
path_condition: Vec::new(),
|
||||||
|
depth: 0,
|
||||||
|
feasible: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a variable's symbolic value.
|
||||||
|
pub fn get(&self, name: &str) -> Option<&SymbolicValue> {
|
||||||
|
self.variables.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a variable's symbolic value.
|
||||||
|
pub fn set(&mut self, name: &str, value: SymbolicValue) {
|
||||||
|
self.variables.insert(name.to_string(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a path constraint.
|
||||||
|
pub fn assume(&mut self, constraint: SymbolicValue) {
|
||||||
|
self.path_condition.push(constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path condition.
|
||||||
|
pub fn path_condition(&self) -> &[SymbolicValue] {
|
||||||
|
&self.path_condition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all variable bindings.
|
||||||
|
pub fn variables(&self) -> &HashMap<String, SymbolicValue> {
|
||||||
|
&self.variables
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forks the state for branching.
|
||||||
|
pub fn fork(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
variables: self.variables.clone(),
|
||||||
|
path_condition: self.path_condition.clone(),
|
||||||
|
depth: self.depth,
|
||||||
|
feasible: self.feasible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increments depth.
|
||||||
|
pub fn increment_depth(&mut self) {
|
||||||
|
self.depth += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns current depth.
|
||||||
|
pub fn depth(&self) -> usize {
|
||||||
|
self.depth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks path as infeasible.
|
||||||
|
pub fn mark_infeasible(&mut self) {
|
||||||
|
self.feasible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if path is still feasible.
|
||||||
|
pub fn is_feasible(&self) -> bool {
|
||||||
|
self.feasible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SymbolicState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Symbolic execution engine.
|
||||||
|
pub struct SymbolicExecutor {
|
||||||
|
config: ProverConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolicExecutor {
|
||||||
|
/// Creates a new symbolic executor.
|
||||||
|
pub fn new(config: ProverConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an initial symbolic state from contract context.
|
||||||
|
pub fn create_initial_state(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
) -> VerifierResult<SymbolicState> {
|
||||||
|
let mut state = SymbolicState::new();
|
||||||
|
|
||||||
|
// Create symbolic values for state variables
|
||||||
|
for (name, var_type) in &ctx.state_vars {
|
||||||
|
let sym = SymbolicValue::symbol(name, var_type.clone());
|
||||||
|
state.set(name, sym);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add msg.sender, msg.value, etc.
|
||||||
|
state.set("msg.sender", SymbolicValue::symbol("msg.sender", VarType::Address));
|
||||||
|
state.set("msg.value", SymbolicValue::symbol("msg.value", VarType::Uint(256)));
|
||||||
|
state.set("block.timestamp", SymbolicValue::symbol("block.timestamp", VarType::Uint(256)));
|
||||||
|
state.set("block.number", SymbolicValue::symbol("block.number", VarType::Uint(256)));
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explores all possible execution paths.
|
||||||
|
pub fn explore_all_paths(
|
||||||
|
&self,
|
||||||
|
ctx: &VerificationContext,
|
||||||
|
) -> VerifierResult<Vec<SymbolicState>> {
|
||||||
|
let initial = self.create_initial_state(ctx)?;
|
||||||
|
let mut completed = Vec::new();
|
||||||
|
let mut worklist = vec![initial];
|
||||||
|
|
||||||
|
while let Some(state) = worklist.pop() {
|
||||||
|
if state.depth() >= self.config.max_depth {
|
||||||
|
// Depth limit reached
|
||||||
|
completed.push(state);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.is_feasible() {
|
||||||
|
// Path is infeasible, skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, just return the initial state
|
||||||
|
// Full implementation would execute each function
|
||||||
|
completed.push(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(completed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Symbolically executes a statement.
|
||||||
|
pub fn execute_statement(
|
||||||
|
&self,
|
||||||
|
stmt: &Statement,
|
||||||
|
state: &mut SymbolicState,
|
||||||
|
) -> VerifierResult<Vec<SymbolicState>> {
|
||||||
|
state.increment_depth();
|
||||||
|
|
||||||
|
if state.depth() >= self.config.max_depth {
|
||||||
|
return Err(VerifierError::Internal("Max depth exceeded".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match stmt {
|
||||||
|
Statement::Assign { target, value } => {
|
||||||
|
let sym_value = self.evaluate_expression(value, state)?;
|
||||||
|
state.set(target, sym_value);
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::If {
|
||||||
|
condition,
|
||||||
|
then_branch,
|
||||||
|
else_branch,
|
||||||
|
} => {
|
||||||
|
let cond = self.evaluate_expression(condition, state)?;
|
||||||
|
|
||||||
|
// Fork state for both branches
|
||||||
|
let mut then_state = state.fork();
|
||||||
|
then_state.assume(cond.clone());
|
||||||
|
|
||||||
|
let mut else_state = state.fork();
|
||||||
|
else_state.assume(SymbolicValue::UnaryOp {
|
||||||
|
op: UnaryOperator::Not,
|
||||||
|
operand: Box::new(cond),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute branches
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for stmt in then_branch {
|
||||||
|
let states = self.execute_statement(stmt, &mut then_state)?;
|
||||||
|
results.extend(states);
|
||||||
|
}
|
||||||
|
|
||||||
|
for stmt in else_branch {
|
||||||
|
let states = self.execute_statement(stmt, &mut else_state)?;
|
||||||
|
results.extend(states);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::While {
|
||||||
|
condition,
|
||||||
|
invariant: _,
|
||||||
|
body,
|
||||||
|
} => {
|
||||||
|
// Bounded loop unrolling
|
||||||
|
let mut current_states = vec![state.clone()];
|
||||||
|
let mut exit_states = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..self.config.max_loop_unroll {
|
||||||
|
let mut next_states = Vec::new();
|
||||||
|
|
||||||
|
for s in current_states.drain(..) {
|
||||||
|
let cond = self.evaluate_expression(condition, &s)?;
|
||||||
|
|
||||||
|
// Continue if condition might be true
|
||||||
|
let mut continue_state = s.fork();
|
||||||
|
continue_state.assume(cond.clone());
|
||||||
|
|
||||||
|
for stmt in body {
|
||||||
|
let results = self.execute_statement(stmt, &mut continue_state)?;
|
||||||
|
next_states.extend(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit if condition might be false
|
||||||
|
let mut exit_state = s;
|
||||||
|
exit_state.assume(SymbolicValue::UnaryOp {
|
||||||
|
op: UnaryOperator::Not,
|
||||||
|
operand: Box::new(cond),
|
||||||
|
});
|
||||||
|
exit_states.push(exit_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if next_states.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_states = next_states;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine remaining loop states with exit states
|
||||||
|
exit_states.extend(current_states);
|
||||||
|
Ok(exit_states)
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Require(expr, _msg) => {
|
||||||
|
let cond = self.evaluate_expression(expr, state)?;
|
||||||
|
state.assume(cond);
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Assert(expr) => {
|
||||||
|
let cond = self.evaluate_expression(expr, state)?;
|
||||||
|
state.assume(cond);
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Assume(expr) => {
|
||||||
|
let cond = self.evaluate_expression(expr, state)?;
|
||||||
|
state.assume(cond);
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Return(_) => {
|
||||||
|
// End of path
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement::Block(stmts) => {
|
||||||
|
let mut current = state.clone();
|
||||||
|
for stmt in stmts {
|
||||||
|
let results = self.execute_statement(stmt, &mut current)?;
|
||||||
|
if results.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
// Take first result (simplification)
|
||||||
|
current = results.into_iter().next().unwrap();
|
||||||
|
}
|
||||||
|
Ok(vec![current])
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// Other statements not yet implemented
|
||||||
|
Ok(vec![state.clone()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates an expression to a symbolic value.
|
||||||
|
pub fn evaluate_expression(
|
||||||
|
&self,
|
||||||
|
expr: &Expression,
|
||||||
|
state: &SymbolicState,
|
||||||
|
) -> VerifierResult<SymbolicValue> {
|
||||||
|
match expr {
|
||||||
|
Expression::Literal(lit) => Ok(SymbolicValue::Concrete(lit.clone())),
|
||||||
|
|
||||||
|
Expression::Variable(name) => {
|
||||||
|
if let Some(val) = state.get(name) {
|
||||||
|
Ok(val.clone())
|
||||||
|
} else {
|
||||||
|
// Unknown variable - create fresh symbol
|
||||||
|
Ok(SymbolicValue::symbol(name, VarType::Uint(256)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::BinaryOp { left, op, right } => {
|
||||||
|
let l = self.evaluate_expression(left, state)?;
|
||||||
|
let r = self.evaluate_expression(right, state)?;
|
||||||
|
Ok(SymbolicValue::binary(l, *op, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::UnaryOp { op, operand } => {
|
||||||
|
let v = self.evaluate_expression(operand, state)?;
|
||||||
|
Ok(SymbolicValue::UnaryOp {
|
||||||
|
op: *op,
|
||||||
|
operand: Box::new(v),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Ternary {
|
||||||
|
condition,
|
||||||
|
then_expr,
|
||||||
|
else_expr,
|
||||||
|
} => {
|
||||||
|
let c = self.evaluate_expression(condition, state)?;
|
||||||
|
let t = self.evaluate_expression(then_expr, state)?;
|
||||||
|
let e = self.evaluate_expression(else_expr, state)?;
|
||||||
|
Ok(SymbolicValue::ite(c, t, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::MemberAccess { object, member } => {
|
||||||
|
// Simplified: create compound symbol name
|
||||||
|
let obj = self.evaluate_expression(object, state)?;
|
||||||
|
let name = format!("{:?}.{}", obj, member);
|
||||||
|
Ok(SymbolicValue::symbol(&name, VarType::Uint(256)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Index { array, index } => {
|
||||||
|
let arr = self.evaluate_expression(array, state)?;
|
||||||
|
let idx = self.evaluate_expression(index, state)?;
|
||||||
|
Ok(SymbolicValue::Select {
|
||||||
|
array: Box::new(arr),
|
||||||
|
index: Box::new(idx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Old(inner) => {
|
||||||
|
// Old values are just the initial symbolic values
|
||||||
|
self.evaluate_expression(inner, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Result => {
|
||||||
|
// Result is a special symbol
|
||||||
|
Ok(SymbolicValue::symbol("__result", VarType::Uint(256)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::FunctionCall { function, args: _ } => {
|
||||||
|
// Model function call result as uninterpreted function
|
||||||
|
let name = format!("{}(...)", function);
|
||||||
|
Ok(SymbolicValue::symbol(&name, VarType::Uint(256)))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// Other expressions
|
||||||
|
Ok(SymbolicValue::symbol("unknown", VarType::Uint(256)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbolic_value_concrete() {
|
||||||
|
let v = SymbolicValue::uint_const(42);
|
||||||
|
assert!(matches!(v.try_evaluate(), Some(Literal::Uint(42))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbolic_value_binary() {
|
||||||
|
let a = SymbolicValue::uint_const(10);
|
||||||
|
let b = SymbolicValue::uint_const(5);
|
||||||
|
let sum = SymbolicValue::binary(a, BinaryOperator::Add, b);
|
||||||
|
|
||||||
|
assert!(matches!(sum.try_evaluate(), Some(Literal::Uint(15))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbolic_state() {
|
||||||
|
let mut state = SymbolicState::new();
|
||||||
|
state.set("x", SymbolicValue::uint_const(100));
|
||||||
|
|
||||||
|
assert!(state.get("x").is_some());
|
||||||
|
assert!(state.get("y").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_state_fork() {
|
||||||
|
let mut state = SymbolicState::new();
|
||||||
|
state.set("x", SymbolicValue::uint_const(1));
|
||||||
|
|
||||||
|
let forked = state.fork();
|
||||||
|
assert!(forked.get("x").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_executor_creation() {
|
||||||
|
let executor = SymbolicExecutor::new(ProverConfig::default());
|
||||||
|
assert_eq!(executor.config.max_depth, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue