diff --git a/.gitignore b/.gitignore index a6604dc95b..855f2ab363 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ rustc-ice-* /index.html # Fixtures -/test-fixtures \ No newline at end of file +/test-fixtures + +node_modules \ No newline at end of file diff --git a/solidity/supra_contracts/README.md b/solidity/supra_contracts/README.md index 53ae762878..e894720164 100644 --- a/solidity/supra_contracts/README.md +++ b/solidity/supra_contracts/README.md @@ -1,6 +1,13 @@ ## Supra EVM Automation Registry -**This repository includes Supra EVM Automation Registry contract and related contracts.** +**This repository includes following smart contracts:** +- MultiSignatureWallet and MultisigBeacon +- ERC20Supra +- BlockMeta +- Automation Registry smart contracts + - AutomationCore: manages configuration, refunds, fee accounting and other helper functions + - AutomationRegistry: user facing contract to register/cancel/stop a task + - AutomationController: manages cycle transition and processing of tasks Foundry consists of: @@ -34,40 +41,8 @@ $ forge build $ forge test ``` -### Format +### Deploying Automation Registry smart contracts ```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +$ forge script script/DeployAutomationRegistry.s.sol:DeployAutomationRegistry --rpc-url --private-key +``` \ No newline at end of file diff --git a/solidity/supra_contracts/deploy_automation_registry.sh b/solidity/supra_contracts/deploy_automation_registry.sh new file mode 100755 index 0000000000..b2ce8d19cd --- /dev/null +++ b/solidity/supra_contracts/deploy_automation_registry.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -e + +source .env +: "${RPC_URL:?Missing RPC_URL in .env}" +: "${PRIVATE_KEY:?Missing PRIVATE_KEY in .env}" + +DEPLOY_LOG="deploy.log" +ENV_FILE="deployed.env" + +# Helper for cleaner + safer extraction +extract() { + local result + result=$(grep -m1 "$1" "$DEPLOY_LOG" | grep -o "0x[a-fA-F0-9]\{40\}") + echo "${result:-NOT_FOUND}" +} + +# ------------------------------------------------------------ +# RUN FOUNDRY DEPLOY SCRIPT +# ------------------------------------------------------------ +echo "" +echo "=== Deploying contracts ===" + +ADDRESS=$(cast wallet address --private-key "$PRIVATE_KEY") +export OWNER=$ADDRESS + +forge script script/DeployERC20Supra.s.sol:DeployERC20Supra \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + --skip-simulation \ + -vvvv > "$DEPLOY_LOG" 2>&1 + +ERC20_SUPRA=$(extract "ERC20Supra deployed at: ") +if [[ "$ERC20_SUPRA" == "NOT_FOUND" ]]; then + echo "ERROR: ERC20Supra address not found" + exit 1 +fi + +export ERC20_SUPRA + +forge script script/DeployAutomationRegistry.s.sol:DeployAutomationRegistry \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --broadcast \ + --skip-simulation \ + -vvvv >> "$DEPLOY_LOG" 2>&1 + +echo "Deployment logs saved to $DEPLOY_LOG" + +# ------------------------------------------------------------ +# PARSE DEPLOYED CONTRACT ADDRESSES +# ------------------------------------------------------------ +echo "" +echo "=== Extracting deployed addresses ===" + +AUTOMATION_CORE_IMPL=$(extract "AutomationCore implementation deployed at:") +AUTOMATION_CORE_PROXY=$(extract "AutomationCore proxy deployed at:") +AUTOMATION_REGISTRY_IMPL=$(extract "AutomationRegistry implementation deployed at:") +AUTOMATION_REGISTRY_PROXY=$(extract "AutomationRegistry proxy deployed at:") +AUTOMATION_CONTROLLER_IMPL=$(extract "AutomationController implementation deployed at:") +AUTOMATION_CONTROLLER_PROXY=$(extract "AutomationController proxy deployed at:") + +# ------------------------------------------------------------ +# WRITE TO .env +# ------------------------------------------------------------ +echo "" +echo "=== Saving contract addresses to $ENV_FILE ===" +echo "" + +cat < "$ENV_FILE" +# Auto-generated deployment output + +ERC20_SUPRA=$ERC20_SUPRA + +AUTOMATION_CORE_IMPL=$AUTOMATION_CORE_IMPL +AUTOMATION_CORE_PROXY=$AUTOMATION_CORE_PROXY + +AUTOMATION_REGISTRY_IMPL=$AUTOMATION_REGISTRY_IMPL +AUTOMATION_REGISTRY_PROXY=$AUTOMATION_REGISTRY_PROXY + +AUTOMATION_CONTROLLER_IMPL=$AUTOMATION_CONTROLLER_IMPL +AUTOMATION_CONTROLLER_PROXY=$AUTOMATION_CONTROLLER_PROXY +EOF + +cat "$ENV_FILE" + +echo "" +echo "=== Deployment Complete ===" \ No newline at end of file diff --git a/solidity/supra_contracts/getTaskDetails.js b/solidity/supra_contracts/getTaskDetails.js new file mode 100755 index 0000000000..d83b44f7b1 --- /dev/null +++ b/solidity/supra_contracts/getTaskDetails.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +import { ethers } from "ethers"; + +const [registryAddress, taskIndex, rpcUrl] = process.argv.slice(2); + +if (!registryAddress || !taskIndex || !rpcUrl) { + console.error("Usage: node getTaskDetails.js "); + process.exit(1); +} + +// Replace with your contract ABI (minimal, only getTaskDetails) +const registryAbi = [ + "function getTaskDetails(uint64 _taskIndex) view returns (tuple(uint128 maxGasAmount,uint128 gasPriceCap,uint128 automationFeeCapForCycle,uint128 lockedFeeForNextCycle,bytes32 txHash,uint64 taskIndex,uint64 registrationTime,uint64 expiryTime,uint64 priority,uint8 taskType,uint8 state,address owner,bytes payloadTx,bytes[] auxData))" +]; + +const provider = new ethers.JsonRpcProvider(rpcUrl); +const registry = new ethers.Contract(registryAddress, registryAbi, provider); + +async function main() { + try { + const task = await registry.getTaskDetails(taskIndex); + console.log(`taskIndex: ${task.taskIndex}`); + console.log(`owner: ${task.owner}`); + console.log(`state: ${["PENDING","ACTIVE","CANCELLED"][task.state]}`); + console.log(`expiryTime: ${task.expiryTime}`); + console.log(`payloadTx: ${task.payloadTx}`); + console.log(`auxData: ${task.auxData}`); + console.log(`maxGasAmount: ${task.maxGasAmount}`); + console.log(`gasPriceCap: ${task.gasPriceCap}`); + console.log(`automationFeeCapForCycle: ${task.automationFeeCapForCycle}`); + console.log(`lockedFeeForNextCycle: ${task.lockedFeeForNextCycle}`); + } catch (e) { + console.error("Error fetching task:", e.message); + } +} + +main(); diff --git a/solidity/supra_contracts/lib/forge-std b/solidity/supra_contracts/lib/forge-std index 27ba11c86a..aeb45e9f32 160000 --- a/solidity/supra_contracts/lib/forge-std +++ b/solidity/supra_contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit 27ba11c86ac93d8d4a50437ae26621468fe63c20 +Subproject commit aeb45e9f32ef8ca78f0aeda17596e9c46374da41 diff --git a/solidity/supra_contracts/lib/openzeppelin-contracts b/solidity/supra_contracts/lib/openzeppelin-contracts index 353f564d1d..8614ef7a24 160000 --- a/solidity/supra_contracts/lib/openzeppelin-contracts +++ b/solidity/supra_contracts/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 353f564d1db53c1d30cfa8a631771c205e41107b +Subproject commit 8614ef7a24d476e37db66054e5237faaf7f43717 diff --git a/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable b/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable index c1f5d81e2f..a73231f64c 160000 --- a/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable +++ b/solidity/supra_contracts/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit c1f5d81e2f53599bc9e4653bbc7c126032c96bd1 +Subproject commit a73231f64c2a4ab1c0bceb43ba8333be45d2df0a diff --git a/solidity/supra_contracts/package-lock.json b/solidity/supra_contracts/package-lock.json new file mode 100644 index 0000000000..765ea6dd30 --- /dev/null +++ b/solidity/supra_contracts/package-lock.json @@ -0,0 +1,131 @@ +{ + "name": "supra_contracts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "dotenv": "^17.2.3", + "ethers": "^6.16.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/solidity/supra_contracts/package.json b/solidity/supra_contracts/package.json new file mode 100644 index 0000000000..db925d0be9 --- /dev/null +++ b/solidity/supra_contracts/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "dotenv": "^17.2.3", + "ethers": "^6.16.0" + }, + "type": "module" +} diff --git a/solidity/supra_contracts/run.sh b/solidity/supra_contracts/run.sh new file mode 100755 index 0000000000..1a8da402ec --- /dev/null +++ b/solidity/supra_contracts/run.sh @@ -0,0 +1,338 @@ +#!/bin/bash +set -e + +source .env + +: "${RPC_URL:?Missing RPC_URL in .env}" +: "${PRIVATE_KEY:?Missing PRIVATE_KEY in .env}" +: "${ADMIN_PRIVATE_KEY:?Missing ADMIN_PRIVATE_KEY in .env}" + +# ------------------------------- +# Load deployed contract addresses +# ------------------------------- +echo "=== Loading deployed contract addresses ===" + +if [ ! -f "deployed.env" ]; then + echo "ERROR: deployed.env not found." + exit 1 +fi + +source deployed.env + +# ------------------------------- +# Validate env variables +# ------------------------------- +: "${ERC20_SUPRA:?Missing ERC20_SUPRA in deployed.env}" +: "${AUTOMATION_CORE_PROXY:?Missing AUTOMATION_CORE_PROXY in deployed.env}" +: "${AUTOMATION_REGISTRY_PROXY:?Missing AUTOMATION_REGISTRY_PROXY in deployed.env}" + +echo "" +echo "Contracts Loaded:" +echo "ERC20_SUPRA: $ERC20_SUPRA" +echo "AUTOMATION_CORE: $AUTOMATION_CORE_PROXY" +echo "AUTOMATION_REGISTRY: $AUTOMATION_REGISTRY_PROXY" + +echo "" +echo "=== Starting Automation CLI ===" + +ERC20_SUPRA="$ERC20_SUPRA" +AUTOMATION_CORE="$AUTOMATION_CORE_PROXY" +REGISTRY="$AUTOMATION_REGISTRY_PROXY" + +ADDRESS=$(cast wallet address --private-key "$PRIVATE_KEY") +echo "" +echo "Using RPC: $RPC_URL" +echo "Wallet: $ADDRESS" +echo "ERC20 Supra: $ERC20_SUPRA" +echo "Automation Core proxy: $AUTOMATION_CORE" +echo "Automation Registry proxy: $REGISTRY" +echo "" + +# ------------------------------- +# Helper - safe send +# ------------------------------- +send_tx() { + cast send \ + --rpc-url "$RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --gas-limit 3000000 \ + "$@" +} + +# ------------------------------- +# Balance + allowance helpers +# ------------------------------- +get_native_balance() { + RAW=$(cast balance "$ADDRESS" --rpc-url "$RPC_URL" 2>/dev/null) + RAW=${RAW:-0} + ETH=$(cast --from-wei "$RAW") + echo "ETH Balance: $ETH ETH" +} + +get_erc20Supra_balance() { + RAW=$(cast erc20-token balance "$ERC20_SUPRA" "$ADDRESS" --rpc-url "$RPC_URL" 2>/dev/null) + DEC_WEI=$(echo "$RAW" | awk '{print $1}') + DEC_WEI=${DEC_WEI:-0} + SUPRA=$(cast --from-wei "$DEC_WEI") + echo "ERC20Supra Balance: $SUPRA SUPRA" +} + +get_allowance() { + RAW=$(cast erc20-token allowance "$ERC20_SUPRA" "$ADDRESS" "$AUTOMATION_CORE" --rpc-url "$RPC_URL" 2>/dev/null) + DEC_WEI=$(echo "$RAW" | awk '{print $1}') + DEC_WEI=${DEC_WEI:-0} + SUPRA=$(cast --from-wei "$DEC_WEI") + echo "Allowance to Automation Registry: $SUPRA SUPRA" +} + +# ------------------------------- +# Registry view functions +# ------------------------------- + +view_task_details() { + echo -n "Task index: " + read -r index + echo "" + echo "=== Task Details ===" + node getTaskDetails.js "$REGISTRY" "$index" "$RPC_URL" + echo "" +} + +is_authorized_submitter() { + echo -n "Enter address: " + read -r address + RAW=$(cast call "$REGISTRY" "isAuthorizedSubmitter(address)(bool)" $address --rpc-url "$RPC_URL") + echo "Is submitter?: $RAW" +} + +view_registry_locked_balance() { + RAW=$(cast call "$REGISTRY" "getTotalLockedBalance()(uint256)" --rpc-url "$RPC_URL") + DEC=$(echo "$RAW" | awk '{print $1}') + SUPRA=$(cast --from-wei "$DEC") + echo "Registry Locked SUPRA: $SUPRA SUPRA" +} + +view_registry_erc20Supra_balance() { + RAW=$(cast erc20-token balance "$ERC20_SUPRA" "$AUTOMATION_CORE" --rpc-url "$RPC_URL") + + DEC=$(echo "$RAW" | awk '{print $1}') + SUPRA=$(cast --from-wei "$DEC") + + echo "Automation Registry ERC20Supra Balance: $SUPRA SUPRA" +} + +view_task_list() { + RAW=$(cast call "$REGISTRY" "getTaskIdList()(uint256[])" --rpc-url "$RPC_URL") + echo "" + echo "=== Task IDs ===" + echo "$RAW" + echo "" +} + +view_total_tasks() { + RAW=$(cast call "$REGISTRY" "totalTasks()(uint256)" --rpc-url "$RPC_URL") + echo "Total Task Count: $RAW" +} + +# ------------------------------- +# Main menu +# ------------------------------- +while true; do + echo "" + echo "Automation Registry CLI" + echo "" + echo "Commands:" + echo " native-balance Show native balance" + echo " erc20Supra-balance Show ERC20Supra balance" + echo " allowance Check ERC20 approval to registry" + echo " nativeToErc20Supra Deposit native → mint ERC20Supra" + echo " nativeToErc20SupraWithAllowance Deposit native to mint ERC20Supra and grant allowance" + echo " approve Approve ERC20Supra for fees" + echo " register Register a user task" + echo " register-system Register a system task" + echo " cancel Cancel a user task" + echo " cancel-system Cancel a system task" + echo " stop Stop user tasks" + echo " stop-system Stop system tasks" + echo " grant-authorization Grant authorization to submit GST" + echo " revoke-authorization Revoke authorization to submit GST" + echo " is-submitter Check if authorized submitter" + echo " task-details View details of a task" + echo " registry-locked-balance View registry's locked balance" + echo " registry-balance View ERC20Supra balance of registry contract" + echo " task-list View all task IDs" + echo " total-tasks View number of tasks" + echo " exit Quit" + echo -n "Command> " + read -r CMD + echo "" + + case "$CMD" in + native-balance) get_native_balance ;; + erc20Supra-balance) get_erc20Supra_balance ;; + allowance) get_allowance ;; + + nativeToErc20Supra) + echo -n "Amount to deposit (ETH): " + read -r ethAmount + weiAmount=$(cast --to-wei "$ethAmount") + echo "Depositing $ethAmount ETH..." + send_tx "$ERC20_SUPRA" "nativeToErc20Supra()" --value "$weiAmount" + ;; + + nativeToErc20SupraWithAllowance) + echo "Enter: " + read -r depositEth spender allowanceEth + + if [ -z "$depositEth" ] || [ -z "$spender" ] || [ -z "$allowanceEth" ]; then + echo "Invalid input. Expected: " + exit 1 + fi + + depositWei=$(cast --to-wei "$depositEth") + allowanceWei=$(cast --to-wei "$allowanceEth") + + echo "Depositing $depositEth ETH, and approving $spender for $allowanceEth ERC20Supra..." + + send_tx "$ERC20_SUPRA" \ + "nativeToErc20SupraWithAllowance(address,uint256)" \ + "$spender" "$allowanceWei" \ + --value "$depositWei" + ;; + + approve) + echo -n "Amount to approve (ETH): " + read -r ethAmount + weiAmount=$(cast --to-wei "$ethAmount") + echo "Approving $ethAmount SUPRA..." + cast erc20-token approve "$ERC20_SUPRA" "$AUTOMATION_CORE" "$weiAmount" --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" + ;; + + register) + echo "Register task (user task)" + echo -n "payloadTx (0x...): " + read -r payloadTx + + echo -n "Duration (seconds): " + read -r duration + now=$(cast block latest --rpc-url "$RPC_URL" | grep "timestamp" | awk '{print $2}') + expiryTime=$(("$now" + "$duration")) + echo "Computed expiryTime = $expiryTime" + + echo -n "txHash (0x...): " + read -r txHash + + echo -n "maxGasAmount: " + read -r maxGas + + echo -n "Gas price cap (GWEI): " + read -r gasPriceCap + gasPriceCapWei=$(cast --to-wei "$gasPriceCap" gwei) # convert GWEI to wei + + + echo -n "Automation fee cap for cycle (ETH): " + read -r feeCap + feeCapWei=$(cast --to-wei "$feeCap") # convert ETH to wei + + echo -n "Priority (uint64): " + read -r priority + + echo -n "Type (uint8): " + read -r taskType + + aux_json="[]" + + send_tx "$REGISTRY" \ + "register(bytes,uint64,bytes32,uint128,uint128,uint128,uint64,uint8,bytes[])" \ + "$payloadTx" "$expiryTime" "$txHash" "$maxGas" "$gasPriceCapWei" "$feeCapWei" "$priority" "$taskType" "$aux_json" + ;; + + register-system) + echo "Register system task" + echo -n "payloadTx (0x...): " + read -r payloadTx + + echo -n "Duration (seconds): " + read -r duration + now=$(cast block latest --rpc-url "$RPC_URL" | grep "timestamp" | awk '{print $2}') + expiryTime=$(("$now" + "$duration")) + echo "Computed expiryTime = $expiryTime" + + echo -n "txHash (0x...): " + read -r txHash + + echo -n "maxGasAmount: " + read -r maxGas + + echo -n "Priority (uint64): " + read -r priority + + echo -n "Type (uint8): " + read -r taskType + + aux_json="[]" + + send_tx "$REGISTRY" \ + "registerSystemTask(bytes,uint64,bytes32,uint128,uint64,uint8,bytes[])" \ + "$payloadTx" "$expiryTime" "$txHash" "$maxGas" "$priority" "$taskType" "$aux_json" + ;; + + cancel) + echo -n "Task index: " + read -r index + send_tx "$REGISTRY" "cancelTask(uint64)" "$index" + ;; + + cancel-system) + echo -n "System task index: " + read -r index + send_tx "$REGISTRY" "cancelSystemTask(uint64)" "$index" + ;; + + stop) + echo -n "Enter task indexes array (e.g. [0,1,2,3]): " + read -r indexes + send_tx "$REGISTRY" "stopTasks(uint64[])" "$indexes" + ;; + + stop-system) + echo -n "System task indexes array (e.g. [0,1,2,3]): " + read -r indexes + send_tx "$REGISTRY" "stopSystemTasks(uint64[])" "$indexes" + ;; + + grant-authorization) + echo -n "Address to grant authorization to: " + read -r -a address + cast send "$REGISTRY" "grantAuthorization(address)" "$address" \ + --rpc-url "$RPC_URL" \ + --private-key "$ADMIN_PRIVATE_KEY" \ + --gas-limit 3000000 + ;; + + revoke-authorization) + echo -n "Address to revoke authorization on: " + read -r -a address + cast send "$REGISTRY" "revokeAuthorization(address)" "$address" \ + --rpc-url "$RPC_URL" \ + --private-key "$ADMIN_PRIVATE_KEY" \ + --gas-limit 3000000 + ;; + + is-submitter) is_authorized_submitter ;; + task-details) view_task_details ;; + registry-locked-balance) view_registry_locked_balance ;; + registry-balance) view_registry_erc20Supra_balance ;; + task-list) view_task_list ;; + total-tasks) view_total_tasks ;; + + exit) + echo "Exiting." + exit 0 + ;; + + *) + echo "Unknown command." + ;; + esac +done diff --git a/solidity/supra_contracts/script/DeployAutomationRegistry.s.sol b/solidity/supra_contracts/script/DeployAutomationRegistry.s.sol new file mode 100644 index 0000000000..5955c1bcc7 --- /dev/null +++ b/solidity/supra_contracts/script/DeployAutomationRegistry.s.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Script, console} from "forge-std/Script.sol"; +import {AutomationCore} from "../src/AutomationCore.sol"; +import {AutomationController} from "../src/AutomationController.sol"; +import {AutomationRegistry} from "../src/AutomationRegistry.sol"; +import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployAutomationRegistry is Script { + uint64 taskDurationCapSecs; + uint128 registryMaxGasCap; + uint128 automationBaseFeeWeiPerSec; + uint128 flatRegistrationFeeWei; + uint8 congestionThresholdPercentage; + uint128 congestionBaseFeeWeiPerSec; + uint8 congestionExponent; + uint16 taskCapacity; + uint64 cycleDurationSecs; + uint64 sysTaskDurationCapSecs; + uint128 sysRegistryMaxGasCap; + uint16 sysTaskCapacity; + address vmSigner; + address erc20Supra; + + // Config values loaded from .env file + function setUp() public { + taskDurationCapSecs = uint64(vm.envUint("TASK_DURATION_CAP_SEC")); + registryMaxGasCap = uint128(vm.envUint("REGISTRY_MAX_GAS_CAP")); + automationBaseFeeWeiPerSec = uint128(vm.envUint("AUTOMATION_BASE_FEE_PER_SEC")); + flatRegistrationFeeWei = uint128(vm.envUint("FLAT_REGISTRATION_FEE")); + congestionThresholdPercentage = uint8(vm.envUint("CONGESTION_THRESHOLD_PERCENTAGE")); + congestionBaseFeeWeiPerSec = uint128(vm.envUint("CONGESTION_BASE_FEE_PER_SEC")); + congestionExponent = uint8(vm.envUint("CONGESTION_EXPONENT")); + taskCapacity = uint16(vm.envUint("TASK_CAPACITY")); + cycleDurationSecs = uint64(vm.envUint("CYCLE_DURATION_SEC")); + sysTaskDurationCapSecs = uint64(vm.envUint("SYS_TASK_DURATION_CAP_SEC")); + sysRegistryMaxGasCap = uint128(vm.envUint("SYS_REGISTRY_MAX_GAS_CAP")); + sysTaskCapacity = uint16(vm.envUint("SYS_TASK_CAPACITY")); + vmSigner = vm.envAddress("VM_SIGNER"); + erc20Supra = vm.envAddress("ERC20_SUPRA"); + } + + function run() public { + vm.startBroadcast(); + + AutomationCore coreImpl; // AutomationCore implementation contract + ERC1967Proxy coreProxy; // AutomationCore proxy contract + AutomationCore automationCore; // Instance of AutomationCore at proxy address + + AutomationRegistry registryImpl; // AutomationRegistry implementation contract + ERC1967Proxy registryProxy; // AutomationRegistry proxy contract + AutomationRegistry registry; // Instance of AutomationRegistry at proxy address + + AutomationController controllerImpl; // AutomationController implementation contract + ERC1967Proxy controllerProxy; // AutomationController proxy contract + AutomationController controller; // Instance of AutomationController at proxy address + + // --------------------------------------------------------------------- + // Deploy AutomationCore + // --------------------------------------------------------------------- + coreImpl = new AutomationCore(); + console.log("AutomationCore implementation deployed at: ", address(coreImpl)); + bytes memory coreInitData = abi.encodeCall( + AutomationCore.initialize, + ( + taskDurationCapSecs, // taskDurationCapSecs + registryMaxGasCap, // registryMaxGasCap + automationBaseFeeWeiPerSec, // automationBaseFeeWeiPerSec + flatRegistrationFeeWei, // flatRegistrationFeeWei + congestionThresholdPercentage, // congestionThresholdPercentage + congestionBaseFeeWeiPerSec, // congestionBaseFeeWeiPerSec + congestionExponent, // congestionExponent + taskCapacity, // taskCapacity + cycleDurationSecs, // cycleDurationSecs + sysTaskDurationCapSecs, // sysTaskDurationCapSecs + sysRegistryMaxGasCap, // sysRegistryMaxGasCap + sysTaskCapacity, // sysTaskCapacity + vmSigner, // VM Signer address + erc20Supra // ERC20Supra address + ) + ); + coreProxy = new ERC1967Proxy(address(coreImpl), coreInitData); + console.log("AutomationCore proxy deployed at: ", address(coreProxy)); + automationCore = AutomationCore(address(coreProxy)); + + // --------------------------------------------------------------------- + // Deploy AutomationRegistry + // --------------------------------------------------------------------- + registryImpl = new AutomationRegistry(); + console.log("AutomationRegistry implementation deployed at: ", address(registryImpl)); + + bytes memory registryInitData = abi.encodeCall(AutomationRegistry.initialize, (address(automationCore))); + registryProxy = new ERC1967Proxy(address(registryImpl), registryInitData); + console.log("AutomationRegistry proxy deployed at: ", address(registryProxy)); + registry = AutomationRegistry(address(registryProxy)); + + // --------------------------------------------------------------------- + // Deploy AutomationController + // --------------------------------------------------------------------- + controllerImpl = new AutomationController(); + console.log("AutomationController implementation deployed at: ", address(controllerImpl)); + + bytes memory controllerInitData = abi.encodeCall( + AutomationController.initialize, + ( + address(automationCore), + address(registry), + true + ) + ); + controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + console.log("AutomationController proxy deployed at: ", address(controllerProxy)); + controller = AutomationController(address(controllerProxy)); + + // -------------------------------------------------------------------------- + // Set AutomationRegistry and AutomationController address in AutomationCore + // -------------------------------------------------------------------------- + automationCore.setAutomationRegistry(address(registry)); + automationCore.setAutomationController(address(controller)); + + // -------------------------------------------------------------------------- + // Set AutomationController address in AutomationRegistry + // -------------------------------------------------------------------------- + registry.setAutomationController(address(controller)); + + vm.stopBroadcast(); + } +} diff --git a/solidity/supra_contracts/src/AutomationController.sol b/solidity/supra_contracts/src/AutomationController.sol new file mode 100644 index 0000000000..5f035786ff --- /dev/null +++ b/solidity/supra_contracts/src/AutomationController.sol @@ -0,0 +1,812 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {EnumerableSet} from "../lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {CommonUtils} from "./CommonUtils.sol"; +import {LibController} from "./LibController.sol"; + +import {IAutomationController} from "./IAutomationController.sol"; +import {IAutomationCore} from "./IAutomationCore.sol"; +import {IAutomationRegistry} from "./IAutomationRegistry.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Ownable2StepUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "../lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract AutomationController is IAutomationController, Ownable2StepUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.UintSet; + using CommonUtils for *; + using LibController for *; + + /// @dev State variables + LibController.AutomationCycleInfo cycleInfo; + address public registry; + address public automationCore; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Emitted when a task is removed as fee exceeds task's automation fee cap for the cycle. + event TaskCancelledCapacitySurpassed( + uint64 indexed taskIndex, + address indexed owner, + uint128 fee, + uint128 automationFeeCapForCycle, + bytes32 registrationHash + ); + + /// @notice Emitted when a task is removed due to insufficient balance. + event TaskCancelledInsufficentBalance( + uint64 indexed taskIndex, + address indexed owner, + uint128 fee, + uint256 balance, + bytes32 registrationHash + ); + + /// @notice Emitted when an automation fee is charged for an automation task for the cycle. + event TaskCycleFeeWithdraw( + uint64 indexed taskIndex, + address indexed owner, + uint128 fee + ); + + /// @notice Emitted when the cycle state transitions. + event AutomationCycleEvent( + uint64 indexed index, + CommonUtils.CycleState indexed state, + uint64 startTime, + uint64 durationSecs, + CommonUtils.CycleState indexed oldState + ); + + /// @notice Event emitted on cycle transition containing active task indexes for the new cycle. + event ActiveTasks(uint256[] indexed taskIndexes); + + /// @notice Event emitted on cycle transition containing removed task indexes. + event RemovedTasks(uint64[] indexed taskIndexes); + + /// @notice Event emitted when on a new cycle inconsistent state of the registry has been identified. + /// When automation is in suspended state, there are no tasks expected. + event ErrorInconsistentSuspendedState(); + + /// @notice Emitted when the AutomationRegistry contract address is updated. + event AutomationRegistryUpdated(address indexed oldRegistryAddress, address indexed newRegistryAddress); + + /// @notice Emitted when the AutomationCore contract address is updated. + event AutomationCoreUpdated(address indexed oldAutomationCore, address indexed newAutomationCore); + + /// @notice Emitted when automation is enabled. + event AutomationEnabled(bool indexed status); + + /// @notice Emitted when automation is disabled. + event AutomationDisabled(bool indexed status); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CONSTRUCTOR AND INITIALIZER :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the configuration parameters of the contract, can only be called once. + /// @param _automationCore Address of the AutomationCore smart contract. + /// @param _registry Address of the AutomationRegistry smart contract. + /// @param _automationEnabled Bool to set automation enabled status. + function initialize(address _automationCore, address _registry, bool _automationEnabled) public initializer { + _automationCore.validateContractAddress(); + _registry.validateContractAddress(); + + automationCore = _automationCore; + registry = _registry; + + (CommonUtils.CycleState state, uint64 cycleId) = _automationEnabled ? (CommonUtils.CycleState.STARTED, 1) : (CommonUtils.CycleState.READY, 0); + + cycleInfo.initializeCycle( + cycleId, + uint64(block.timestamp), + IAutomationCore(_automationCore).cycleDurationSecs(), + state, + _automationEnabled + ); + + __Ownable2Step_init(); + __Ownable_init(msg.sender); + } + + /// @notice Called by the VM Signer on `AutomationBookkeepingAction::Process` action emitted by native layer ahead of the cycle transition. + /// @param _cycleIndex Index of the cycle. + /// @param _taskIndexes Array of task index to be processed. + function processTasks(uint64 _cycleIndex, uint64[] memory _taskIndexes) external { + // Check caller is VM Signer + if (msg.sender != IAutomationCore(automationCore).getVmSigner()) { revert CallerNotVmSigner(); } + + CommonUtils.CycleState state = cycleInfo.state(); + + if(state == CommonUtils.CycleState.FINISHED) { + onCycleTransition(_cycleIndex, _taskIndexes); + } else { + if(state != CommonUtils.CycleState.SUSPENDED) { revert InvalidRegistryState(); } + onCycleSuspend(_cycleIndex, _taskIndexes); + } + } + + /// @notice Checks the cycle end and emit an event on it. Does nothing if SUPRA_NATIVE_AUTOMATION or SUPRA_AUTOMATION_V2 is disabled. + function monitorCycleEnd() external { + if (tx.origin != IAutomationCore(automationCore).getVmSigner()) { revert CallerNotVmSigner(); } + + if(!isCycleStarted() || getCycleEndTime() > block.timestamp) { + return; + } + + onCycleEndInternal(); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: HELPER FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Traverses the list of the tasks and based on the task state and expiry information either charges or drops the task after refunding eligable fees. + /// Tasks are checked not to be processed more than once. + /// This function should be called only if registry is in FINISHED state, meaning a normal cycle transition is happening. + /// After processing all input tasks, intermediate transition state is updated and transition end is checked (whether all expected tasks has been processed already). + /// In case if transition end is detected a start of the new cycle is given (if during trasition period suspention is not requested) and corresponding event is emitted. + /// @param _cycleIndex Cycle index of the new cycle to which the transition is being done. + /// @param _taskIndexes Array of task indexes to be processed. + function onCycleTransition(uint64 _cycleIndex, uint64[] memory _taskIndexes) private { + if(_taskIndexes.length == 0) { return; } + + if(cycleInfo.state() != CommonUtils.CycleState.FINISHED) { revert InvalidRegistryState(); } + + // Check if transition state exists + if(!cycleInfo.ifTransitionStateExists()) { revert InvalidRegistryState(); } + if(cycleInfo.index() + 1 != _cycleIndex) { revert InvalidInputCycleIndex(); } + + LibController.IntermediateStateOfCycleChange memory intermediateState = dropOrChargeTasks(_taskIndexes); + + cycleInfo.transitionState.lockedFees += intermediateState.cycleLockedFees; + cycleInfo.setGasCommittedForNextCycle(cycleInfo.gasCommittedForNextCycle() + intermediateState.gasCommittedForNextCycle); + cycleInfo.setSysGasCommittedForNextCycle(cycleInfo.sysGasCommittedForNextCycle() + intermediateState.sysGasCommittedForNextCycle); + + updateCycleTransitionStateFromFinished(); + if(intermediateState.removedTasks.length > 0) { + emit RemovedTasks(intermediateState.removedTasks); + } + } + + /// @notice Traverses the list of the tasks and refunds automation(if not PENDING) and deposit fees for all tasks and removes from registry. + /// This function is called only if automation feature is disabled, i.e. cycle is in SUSPENDED state. + /// After processing input set of tasks the end of suspention process is checked(i.e. all expected tasks have been processed). + /// In case if end is identified, the registry state is update to READY and corresponding event is emitted. + /// @param _cycleIndex Input cycle index of the cycle being suspended. + /// @param _taskIndexes Array of task indexes to be processed. + function onCycleSuspend(uint64 _cycleIndex, uint64[] memory _taskIndexes) private { + if (_taskIndexes.length == 0) { return; } + + if(cycleInfo.state() != CommonUtils.CycleState.SUSPENDED) { revert InvalidRegistryState(); } + if(cycleInfo.index() != _cycleIndex) { revert InvalidInputCycleIndex(); } + // Check if transition state exists + if(!cycleInfo.ifTransitionStateExists()) { revert InvalidRegistryState(); } + + uint64 currentTime = uint64(block.timestamp); + + // Sort task indexes as order is important + uint64[] memory taskIndexes = _taskIndexes.sortUint64(); + uint64[] memory removedTasks = new uint64[](taskIndexes.length); + + IAutomationRegistry automationRegistry = IAutomationRegistry(registry); + uint64 removedCounter; + for (uint i = 0; i < taskIndexes.length; i++) { + if(automationRegistry.ifTaskExists(taskIndexes[i])) { + CommonUtils.TaskDetails memory task = automationRegistry.getTaskDetails(taskIndexes[i]); + + (bool removed, ) = address(automationRegistry).call(abi.encodeCall(IAutomationRegistry.removeTask, (taskIndexes[i], false))); + require(removed, RemoveTaskFailed()); + + removedTasks[removedCounter++] = taskIndexes[i]; + markTaskProcessed(taskIndexes[i]); + + // Nothing to refund for GST tasks + if (task.taskType == CommonUtils.TaskType.UST) { + (bool refunded, ) = automationCore.call( + abi.encodeCall( + IAutomationCore.refundTaskFees, + (currentTime, cycleInfo.refundDuration(), cycleInfo.automationFeePerSec(), task) + ) + ); + require(refunded, RefundFailed()); + } + } + } + + updateCycleTransitionStateFromSuspended(); + emit RemovedTasks(removedTasks); + } + + /// @notice Traverses all input task indexes and either drops or tries to charge automation fee if possible. + /// @param _taskIndexes Input task indexes. + /// @return intermediateState Returns the intermediate state. + function dropOrChargeTasks( + uint64[] memory _taskIndexes + ) private returns (LibController.IntermediateStateOfCycleChange memory intermediateState) { + uint64 currentTime = uint64(block.timestamp); + uint64 currentCycleEndTime = currentTime + cycleInfo.newCycleDuration(); + + // Sort task indexes to charge automation fees in their chronological order + uint64[] memory taskIndexes = _taskIndexes.sortUint64(); + + uint64[] memory removedBuffer = new uint64[](taskIndexes.length); + uint256 removedCount; + + // Process each active task and calculate fee for the cycle for the tasks + for (uint256 i = 0; i < taskIndexes.length; i++) { + LibController.TransitionResult memory result = dropOrChargeTask( + taskIndexes[i], + currentTime, + currentCycleEndTime + ); + + if (result.isRemoved) { + removedBuffer[removedCount] = taskIndexes[i]; + removedCount += 1; + } + + intermediateState.gasCommittedForNextCycle += result.gas; + intermediateState.sysGasCommittedForNextCycle += result.sysGas; + intermediateState.cycleLockedFees += result.fees; + } + + uint64[] memory removedTasks = new uint64[](removedCount); + for (uint256 j = 0; j < removedCount; j++) { + removedTasks[j] = removedBuffer[j]; + } + intermediateState.removedTasks = removedTasks; + } + + /// @notice Drops or charges the input task. If the task is already processed or missing from the registry then nothing is done. + /// @param _taskIndex Task index to be dropped or charged. + /// @param _currentTime Current time. + /// @param _currentCycleEndTime End time of the current cycle. + /// @return result Returns the TransitionResult. + function dropOrChargeTask( + uint64 _taskIndex, + uint64 _currentTime, + uint64 _currentCycleEndTime + ) private returns (LibController.TransitionResult memory result){ + address registryAddr = registry; + if(IAutomationRegistry(registryAddr).ifTaskExists(_taskIndex)) { + markTaskProcessed(_taskIndex); + + CommonUtils.TaskDetails memory task = IAutomationRegistry(registryAddr).getTaskDetails(_taskIndex); + bool isUst = task.taskType == CommonUtils.TaskType.UST; + + // Task is cancelled or expired + if(task.state == CommonUtils.TaskState.CANCELLED || _currentTime >= task.expiryTime) { + if(isUst) { + (bool sent, ) = registryAddr.call( + abi.encodeCall( + IAutomationRegistry.refundDepositAndDrop, + (_taskIndex, task.owner, task.depositFee, task.depositFee) + ) + ); + require(sent, RefundDepositAndDropFailed()); + } else { + // Remove the task from registry and system registry + (bool removed, ) = registryAddr.call(abi.encodeCall(IAutomationRegistry.removeTask, (_taskIndex, true))); + require(removed, RemoveTaskFailed()); + } + result.isRemoved = true; + } else if(!isUst) { + // Active GST + // Governance submitted tasks are not charged + + result.sysGas = task.maxGasAmount; + (bool updated, ) = registryAddr.call(abi.encodeCall(IAutomationRegistry.updateTaskState, (_taskIndex, CommonUtils.TaskState.ACTIVE))); + require(updated, UpdateTaskStateFailed()); + } else { + // Active UST + uint128 fee = IAutomationCore(automationCore).calculateTaskFee( + task.state, + task.expiryTime, + task.maxGasAmount, + cycleInfo.newCycleDuration(), + _currentTime, + cycleInfo.automationFeePerSec() + ); + + // If the task reached this phase that means it is a valid active task for the new cycle. + // During cleanup all expired tasks has been removed from the registry but the state of the tasks is not updated. + // As here we need to distinguish new tasks from already existing active tasks, + // as the fee calculation for them will be different based on their active duration in the cycle. + // For more details see calculateTaskFee function. + (bool updated, ) = registryAddr.call(abi.encodeCall(IAutomationRegistry.updateTaskState, (_taskIndex, CommonUtils.TaskState.ACTIVE))); + require(updated, UpdateTaskStateFailed()); + + (result.isRemoved, result.gas, result.fees) = tryWithdrawTaskAutomationFee( + _taskIndex, + task.owner, + task.maxGasAmount, + task.expiryTime, + task.depositFee, + fee, + _currentCycleEndTime, + task.automationFeeCapForCycle, + task.txHash + ); + } + } + } + + /// @notice Marks a task as processed. + /// @param _taskIndex Index of the task to be marked as processed. + function markTaskProcessed(uint64 _taskIndex) private { + uint64 nextTaskIndexPosition = cycleInfo.nextTaskIndexPosition(); + + if(nextTaskIndexPosition >= cycleInfo.transitionState.expectedTasksToBeProcessed.length()) { revert InconsistentTransitionState(); } + uint64 expectedTask = uint64(cycleInfo.transitionState.expectedTasksToBeProcessed.at(nextTaskIndexPosition)); + + if(expectedTask != _taskIndex) { revert OutOfOrderTaskProcessingRequest(); } + cycleInfo.setNextTaskIndexPosition(nextTaskIndexPosition + 1); + } + + /// @notice Helper function to withdraw automation task fees for an active task. + /// @param _taskIndex Index of the task. + /// @param _owner Owner of the task. + /// @param _maxGasAmount Max gas amount of the task. + /// @param _expiryTime Expiry time of the task. + /// @param _depositFee Deposit fees of the task. + /// @param _fee Fees to be charged for the task. + /// @param _currentCycleEndTime End time of the current cycle. + /// @param _automationFeeCapForCycle Max automation fee for a cycle to be paid. + /// @param _regHash Tx hash of the task. + /// @return Bool representing if the task was removed. + /// @return Amount to add to gasCommittedForNextCycle + /// @return Amount to add to cycleLockedFees + function tryWithdrawTaskAutomationFee( + uint64 _taskIndex, + address _owner, + uint128 _maxGasAmount, + uint64 _expiryTime, + uint128 _depositFee, + uint128 _fee, + uint64 _currentCycleEndTime, + uint128 _automationFeeCapForCycle, + bytes32 _regHash + ) private returns (bool, uint128, uint128) { + // Remove the automation task if the cycle fee cap is exceeded. + // It might happen that task has been expired by the time charging is being done. + // This may be caused by the fact that bookkeeping transactions has been withheld due to cycle transition. + + address automationCoreAddr = automationCore; + address erc20Supra = IAutomationCore(automationCoreAddr).erc20Supra(); + bool isRemoved; + uint128 gas; + uint128 fees; + address registryAddr = registry; + if(_fee > _automationFeeCapForCycle) { + (bool sent, ) = registryAddr.call( + abi.encodeCall( + IAutomationRegistry.refundDepositAndDrop, + (_taskIndex, _owner, _depositFee, _depositFee) + ) + ); + require(sent, RefundDepositAndDropFailed()); + + isRemoved = true; + + emit TaskCancelledCapacitySurpassed( + _taskIndex, + _owner, + _fee, + _automationFeeCapForCycle, + _regHash + ); + } else { + uint256 userBalance = IERC20(erc20Supra).balanceOf(_owner); + if(userBalance < _fee) { + // If the user does not have enough balance, remove the task, DON'T refund the locked deposit, but simply unlock it and emit an event. + + (bool unlocked, ) = automationCoreAddr.call( + abi.encodeCall( + IAutomationCore.safeUnlockLockedDeposit, + (_taskIndex, _depositFee) + ) + ); + require(unlocked, UnlockLockedDepositFailed()); + + (bool removed, ) = registryAddr.call(abi.encodeCall(IAutomationRegistry.removeTask, (_taskIndex, false))); + require(removed, RemoveTaskFailed()); + isRemoved = true; + + emit TaskCancelledInsufficentBalance( + _taskIndex, + _owner, + _fee, + userBalance, + _regHash + ); + } else { + if(_fee != 0) { + // Charge the fee + (bool sent, ) = automationCoreAddr.call(abi.encodeCall(IAutomationCore.chargeFees, (_owner, _fee))); + if (!sent) { revert TransferFailed(); } + + fees = _fee; + } + + emit TaskCycleFeeWithdraw( + _taskIndex, + _owner, + _fee + ); + + // Calculate gas commitment for the next cycle only for valid active tasks + if (_expiryTime > _currentCycleEndTime) { + gas = _maxGasAmount; + } + } + } + + return (isRemoved, gas, fees); + } + + /// @notice Updates the cycle state if the transition is identified to be finalized. + /// From FINISHED state we always move to the next cycle and in STARTED state. + /// But if it happened so that there was a suspension during cycle transition which was ignored, then immediately cycle state is updated to suspended. + /// Expectation will be that native layer catches this double transition and issues refund for the new cycle fees which will not be proceeded further in any case. + function updateCycleTransitionStateFromFinished() private { + // Check if transition state exists + if(!cycleInfo.ifTransitionStateExists()) { revert InvalidRegistryState(); } + + bool transitionFinalized = isTransitionFinalized(); + if (transitionFinalized) { + if (!cycleInfo.automationEnabled() && cycleInfo.state() == CommonUtils.CycleState.FINISHED) { + tryMoveToSuspendedState(); + } else { + (bool updated, ) = automationCore.call( + abi.encodeCall( + IAutomationCore.updateGasCommittedAndCycleLockedFees, + ( + cycleInfo.transitionState.lockedFees, + cycleInfo.sysGasCommittedForNextCycle(), + cycleInfo.gasCommittedForNextCycle(), + cycleInfo.gasCommittedForNewCycle() + ) + ) + ); + require(updated, UpdateGasCommittedAndCycleLockedFeesFailed()); + + IAutomationRegistry automationRegistry = IAutomationRegistry(registry); + automationRegistry.updateTaskIds(CommonUtils.CycleState.FINISHED); + + // Set current timestamp as cycle start time + // Increment the cycle and update the state to STARTED + moveToStartedState(); + if(automationRegistry.getTotalActiveTasks() > 0 ) { + uint256[] memory activeTasks = automationRegistry.getAllActiveTaskIds(); + emit ActiveTasks(activeTasks); + } + } + } + } + + /// @notice Updates the cycle state if the transition is identified to be finalized. + /// As transition happens from suspended state and while transition was in progress + /// - if the feature was enabled back, then the transition will happen direclty to STARTED state, + /// - otherwise the transition will be done to the READY state. + /// + /// In both cases config will be updated. In this case we will make sure to keep the consistency of state + /// when transition to READY state happens through paths + /// - Started -> Suspended -> Ready + /// - or Started-> {Finished, Suspended} -> Ready + /// - or Started -> Finished -> {Started, Suspended} + function updateCycleTransitionStateFromSuspended() private { + // Check if transition state exists + if(!cycleInfo.ifTransitionStateExists()) { revert InvalidRegistryState(); } + if(!isTransitionFinalized()) { + return; + } + + (bool updated, )= automationCore.call(abi.encodeCall(IAutomationCore.updateGasCommittedAndCycleLockedFees, (0, 0, 0, 0))); + require(updated, UpdateGasCommittedAndCycleLockedFeesFailed()); + + IAutomationRegistry(registry).updateTaskIds(CommonUtils.CycleState.SUSPENDED); + + // Check if automation is enabled + if (cycleInfo.automationEnabled()) { + // Update the config in case if transition flow is STARTED -> SUSPENDED-> STARTED. + // to reflect new configs for the new cycle if it has been updated during SUSPENDED state processing + updateConfigFromBuffer(); + moveToStartedState(); + } else { + moveToReadyState(); + } + } + + /// @notice Transition to suspended state is expected to be called + /// a) when cycle is active and in progress + /// - here we simply move to suspended state so native layer can start requesting tasks processing + /// which will end up in refunds and cleanup. Note that refund will be done based on total gas-committed + /// for the current cycle defined at the begining for the cycle, and using current automation fee parameters + /// b) when cycle has just finished and there was another transaction causing feature suspension + /// - as this both events happen in scope of the same block, then we will simply update the state to suspended + /// and the native layer should identify the transition and request processing of the all available tasks. + /// Note that in this case automation fee refund will not be expected and suspention and cycle end matched and + /// no fee was yet charged to be refunded. + /// So the duration for refund and automation-fee-per-second for refund will be 0 + /// c) when cycle transition was in progress and there was a feature suspension, but it could not be applied, + /// and postponed till the cycle transition concludes + /// In all the cases if there are no tasks in registry the state will be updated directly to READY state. + function tryMoveToSuspendedState() private { + IAutomationRegistry automationRegistry = IAutomationRegistry(registry); + if(automationRegistry.totalTasks() == 0) { + // Registry is empty move to ready state directly + updateCycleStateTo(CommonUtils.CycleState.READY); + } else if (!cycleInfo.ifTransitionStateExists()) { + // Indicates that cycle was in STARTED state when suspention has been identified. + // It is safe to assert that cycleEndTime will always be greater than current chain time as + // the cycle end is check in the block metadata txn execution which proceeds any other transaction in the block. + // Including the transaction which caused transition to suspended state. + // So in case if cycleEndTime < currentTime then cycle end would have been identified + // and we would have enterend else branch instead. + // This holds true even if we identified suspention when moving from FINALIZED->STARTED state. + // As in this case we will first transition to the STARTED state and only then to SUSPENDED. + // And when transition to STARTED state we update the cycle start-time to be the current-chain-time. + uint64 currentTime = uint64(block.timestamp); + uint64 cycleEndTime = getCycleEndTime(); + + if(currentTime < cycleInfo.startTime()) { revert InvalidRegistryState(); } + if(currentTime >= cycleEndTime) { revert InvalidRegistryState(); } + if(!isCycleStarted()) { revert InvalidRegistryState(); } + + uint256[] memory expectedTasksToBeProcessed = automationRegistry.getTaskIdList().sortUint256(); + + cycleInfo.setRefundDuration(cycleEndTime - currentTime); + cycleInfo.setNewCycleDuration(cycleInfo.durationSecs()); + cycleInfo.setAutomationFeePerSec(IAutomationCore(automationCore).calculateAutomationFeeMultiplierForCurrentCycleInternal()); + cycleInfo.setGasCommittedForNewCycle(0); + cycleInfo.setGasCommittedForNextCycle(0); + cycleInfo.setSysGasCommittedForNextCycle(0); + cycleInfo.transitionState.lockedFees = 0; + cycleInfo.setNextTaskIndexPosition(0); + + updateExpectedTasks(expectedTasksToBeProcessed); + cycleInfo.setTransitionStateExists(true); + + updateCycleStateTo(CommonUtils.CycleState.SUSPENDED); + } else { + if(cycleInfo.state() != CommonUtils.CycleState.FINISHED) { revert InvalidRegistryState(); } + if(isTransitionInProgress()) { revert InvalidRegistryState(); } + + // Did not manage to charge cycle fee, so automationFeePerSec will be 0 along with remaining duration + // So the tasks sent for refund, will get only deposit refunded. + cycleInfo.setRefundDuration(0); + cycleInfo.setAutomationFeePerSec(0); + cycleInfo.setGasCommittedForNewCycle(0); + + updateCycleStateTo(CommonUtils.CycleState.SUSPENDED); + } + } + + /// @notice Transitions cycle state to the READY state. + function moveToReadyState() private { + // If the cycle duration updated has been identified during transtion, then the transition state is kept + // with reset values except new cycle duration to have it properly set for the next new cycle. + // This may happen in case if cycle was ended and feature-flag has been disbaled before any task has + // been processed for the cycle transition. + // Note that we want to have consistent data in ready state which says that the cycle pointed in the ready state + // has been finished/summerized, and we are ready to start the next new cycle, and all the cycle information should + // match the finalized/summerized cycle since its start, including cycle duration. + + // Check if transition state exists + if(cycleInfo.ifTransitionStateExists()) { + if (cycleInfo.newCycleDuration() == cycleInfo.durationSecs()) { + // Delete transition state + cycleInfo.transitionState.expectedTasksToBeProcessed.clear(); + delete cycleInfo.transitionState; + cycleInfo.setTransitionStateExists(false); + } else { + // Reset all except new cycle duration + cycleInfo.setRefundDuration(0); + cycleInfo.setAutomationFeePerSec(0); + cycleInfo.setGasCommittedForNewCycle(0); + cycleInfo.setGasCommittedForNextCycle(0); + cycleInfo.setSysGasCommittedForNextCycle(0); + cycleInfo.transitionState.lockedFees = 0; + cycleInfo.setNextTaskIndexPosition(0); + cycleInfo.transitionState.expectedTasksToBeProcessed.clear(); + } + } + updateCycleStateTo(CommonUtils.CycleState.READY); + } + + /// @notice Transitions cycle state to the STARTED state. + function moveToStartedState() private { + cycleInfo.setIndex(cycleInfo.index() + 1); + + cycleInfo.setStartTime(uint64(block.timestamp)); + + // Check if the transition state exists + if(cycleInfo.ifTransitionStateExists()) { + cycleInfo.setDurationSecs(cycleInfo.newCycleDuration()); + } + + updateCycleStateTo(CommonUtils.CycleState.STARTED); + } + + /// @notice Updates the state of the cycle. + /// @param _state Input state to update cycle state with. + function updateCycleStateTo(CommonUtils.CycleState _state) private { + CommonUtils.CycleState oldState = cycleInfo.state(); + cycleInfo.setState(uint8(_state)); + + emit AutomationCycleEvent ( + cycleInfo.index(), + cycleInfo.state(), + cycleInfo.startTime(), + cycleInfo.durationSecs(), + oldState + ); + } + + /// @notice Helper function to update the expected tasks of the transition state. + function updateExpectedTasks(uint256[] memory _expectedTasks) private { + cycleInfo.transitionState.expectedTasksToBeProcessed.clear(); + + for (uint256 i = 0; i < _expectedTasks.length; i++) { + cycleInfo.transitionState.expectedTasksToBeProcessed.add(_expectedTasks[i]); + } + } + + /// @notice Helper function called when cycle end is identified. + function onCycleEndInternal() private { + if (!cycleInfo.automationEnabled()) { + tryMoveToSuspendedState(); + } else{ + IAutomationRegistry automationRegistry = IAutomationRegistry(registry); + if(automationRegistry.totalTasks() == 0) { + // Registry is empty update config buffer and move to STARTED state directly + updateConfigFromBuffer(); + moveToStartedState(); + } else { + IAutomationCore core = IAutomationCore(automationCore); + uint256[] memory expectedTasksToBeProcessed = automationRegistry.getTaskIdList().sortUint256(); + + // Updates transition state + cycleInfo.setRefundDuration(0); + cycleInfo.setNewCycleDuration(cycleInfo.durationSecs()); + cycleInfo.setGasCommittedForNewCycle(core.getGasCommittedForNextCycle()); + cycleInfo.setGasCommittedForNextCycle(0); + cycleInfo.setSysGasCommittedForNextCycle (0); + cycleInfo.transitionState.lockedFees = 0; + cycleInfo.setNextTaskIndexPosition(0); + updateExpectedTasks(expectedTasksToBeProcessed); + + cycleInfo.setTransitionStateExists(true); + + // During cycle transition we update config only after transition state is created in order to have new cycle duration as transition state parameter. + updateConfigFromBuffer(); + + // Calculate automation fee per second for the new cycle only after configuration is updated. + // As we already know the committed gas for the new cycle it is being calculated using updated fee parameters + // and will be used to charge tasks during transition process. + cycleInfo.setAutomationFeePerSec(core.calculateAutomationFeeMultiplierForCommittedOccupancy(cycleInfo.gasCommittedForNewCycle())); + updateCycleStateTo(CommonUtils.CycleState.FINISHED); + } + } + } + + /// @notice Function to update the registry config structure with values extracted from the buffer, if the buffer exists. + function updateConfigFromBuffer() private { + (bool applied, uint64 cycleDuration) = IAutomationCore(automationCore).applyPendingConfig(); + if (!applied) return; + + // Check if transition state exists + if (cycleInfo.ifTransitionStateExists()) { + cycleInfo.setNewCycleDuration(cycleDuration); + } else { + cycleInfo.setDurationSecs(cycleDuration); + } + } + + /// @notice Checks if the cycle transition is finalized. + /// @return Bool representing if the cycle transition is finalized. + function isTransitionFinalized() private view returns (bool) { + return cycleInfo.transitionState.expectedTasksToBeProcessed.length() == cycleInfo.nextTaskIndexPosition(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Checks if the cycle transition is in progress. + /// @return Bool representing if the cycle transition is in progress. + function isTransitionInProgress() public view returns (bool) { + return cycleInfo.nextTaskIndexPosition() != 0; + } + + /// @notice Checks whether cycle is in STARTED state. + function isCycleStarted() public view returns (bool) { + return cycleInfo.state() == CommonUtils.CycleState.STARTED; + } + + /// @notice Returns the index, start time, duration and state of the current cycle. + function getCycleInfo() external view returns (uint64, uint64, uint64, CommonUtils.CycleState) { + return (cycleInfo.index(), cycleInfo.startTime(), cycleInfo.durationSecs(), cycleInfo.state()); + } + + /// @notice Returns the duration of the current cycle. + function getCycleDuration() external view returns (uint64) { + return cycleInfo.durationSecs(); + } + + /// @notice Returns the refund duration and automation fee per sec of the transtition state. + /// @return Refund duration + /// @return Automation fee per sec + function getTransitionInfo() external view returns (uint64, uint128) { + return (cycleInfo.refundDuration(), cycleInfo.automationFeePerSec()); + } + + /// @notice Returns if automation is enabled. + function isAutomationEnabled() external view returns (bool) { + return cycleInfo.automationEnabled(); + } + + /// @notice Returns the cycle end time. + function getCycleEndTime() public view returns (uint64 cycleEndTime) { + cycleEndTime = cycleInfo.startTime() + cycleInfo.durationSecs(); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ADMIN FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function to update the AutomationRegistry contract address. + /// @param _registry Address of the AutomationRegistry contract. + function setAutomationRegistry(address _registry) external onlyOwner { + _registry.validateContractAddress(); + + address oldRegistry = registry; + registry = _registry; + + emit AutomationRegistryUpdated(oldRegistry, _registry); + } + + /// @notice Function to update the AutomationCore contract address. + /// @param _automationCore Address of the AutomationCore contract. + function setAutomationCore(address _automationCore) external onlyOwner { + _automationCore.validateContractAddress(); + + address oldAutomationCore = automationCore; + automationCore = _automationCore; + + emit AutomationCoreUpdated(oldAutomationCore, _automationCore); + } + + /// @notice Function to enable the automation. + function enableAutomation() external onlyOwner { + if (cycleInfo.automationEnabled()) { revert AlreadyEnabled(); } + + cycleInfo.setAutomationEnabled(true); + + if (cycleInfo.state() == CommonUtils.CycleState.READY) { + moveToStartedState(); + updateConfigFromBuffer(); + } + + emit AutomationEnabled(cycleInfo.automationEnabled()); + } + + /// @notice Function to disable the automation. + function disableAutomation() external onlyOwner { + if(!cycleInfo.automationEnabled()) { revert AlreadyDisabled(); } + + cycleInfo.setAutomationEnabled(false); + + if (cycleInfo.state() == CommonUtils.CycleState.FINISHED && !isTransitionInProgress()) { + tryMoveToSuspendedState(); + } + + emit AutomationDisabled(cycleInfo.automationEnabled()); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/src/AutomationCore.sol b/solidity/supra_contracts/src/AutomationCore.sol new file mode 100644 index 0000000000..313e34d109 --- /dev/null +++ b/solidity/supra_contracts/src/AutomationCore.sol @@ -0,0 +1,1027 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {CommonUtils} from "./CommonUtils.sol"; +import {LibConfig} from "./LibConfig.sol"; + +import {IAutomationCore} from "./IAutomationCore.sol"; +import {IAutomationController} from "./IAutomationController.sol"; +import {IAutomationRegistry} from "./IAutomationRegistry.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Ownable2StepUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "../lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract AutomationCore is IAutomationCore, Ownable2StepUpgradeable, UUPSUpgradeable { + using CommonUtils for *; + using LibConfig for *; + + /// @dev Constant for 10^8 + uint256 constant DECIMAL = 100_000_000; + + /// @dev Constants describing REFUND TYPE + uint8 constant DEPOSIT_CYCLE_FEE = 0; + uint8 constant CYCLE_FEE = 1; + + /// @dev Refund fraction + uint8 constant REFUND_FRACTION = 2; + + /// @dev State variables + LibConfig.ConfigBuffer configBuffer; + LibConfig.RegistryConfig regConfig; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Emitted when a new config is added. + event ConfigBufferUpdated(LibConfig.ConfigDetails indexed pendingConfig); + + /// @notice Emitted when task registration is enabled. + event TaskRegistrationEnabled(bool indexed status); + + /// @notice Emitted when task registration is disabled. + event TaskRegistrationDisabled(bool indexed status); + + /// @notice Emitted when the VM Signer address is updated. + event VmSignerUpdated(address indexed oldVmSigner, address indexed newVmSigner); + + /// @notice Emitted when the ERC20Supra address is updated. + event Erc20SupraUpdated(address indexed oldErc20Supra, address indexed newErc20Supra); + + /// @notice Emitted when the automation controller smart contract address is updated. + event AutomationControllerUpdated(address indexed oldController, address indexed newController); + + /// @notice Emitted when the automation registry smart contract address is updated. + event AutomationRegistryUpdated(address indexed oldRegistry, address indexed newRegistry); + + /// @notice Emitted when the registry fees is withdrawn by the admin. + event RegistryFeeWithdrawn(address indexed recipient, uint256 indexed feesWithdrawn); + + /// @notice Emitted when deposit fee is being refunded but total locked deposits is less than the locked deposit for the task. + event ErrorUnlockTaskDepositFee( + uint64 indexed taskIndex, + uint256 indexed totalDepositedAutomationFees, + uint128 indexed lockedDeposit + ); + + /// @notice Emitted during cycle transition when refunds to be paid is not possible due to insufficient contract balance. + /// Type of the refund can be related either to the deposit paid during registration (0), or to cycle fee caused by + /// the shortening of the cycle (1) + event ErrorInsufficientBalanceToRefund( + uint64 indexed _taskIndex, + address indexed _owner, + uint8 indexed _refundType, + uint128 _amount + ); + + /// @notice Emitted when a deposit fee is refunded for an automation task. + event TaskDepositFeeRefund(uint64 indexed taskIndex, address owner, uint128 amount); + + /// @notice Emitted when an automation fee is refunded for an automation task at the end of the cycle for excessive + /// duration paid at the beginning of the cycle due to cycle duration reduction by governance. + event TaskFeeRefund( + uint64 indexed taskIndex, + address indexed owner, + uint64 indexed amount + ); + + /// @notice Emitted when a task cycle fee is being refunded but locked cycle fees is less than the requested refund. + event ErrorUnlockTaskCycleFee( + uint64 indexed taskIndex, + uint256 indexed lockedCycleFees, + uint64 indexed refund + ); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CONSTRUCTOR AND INITIALIZER :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the configuration parameters of the registry, can only be called once. + /// @param _taskDurationCapSecs Maximum allowable duration (in seconds) from the registration time that a user automation task can run. + /// @param _registryMaxGasCap Maximum gas allocation for automation tasks per cycle. + /// @param _automationBaseFeeWeiPerSec Base fee per second for the full capacity of the automation registry, measured in wei/sec. + /// @param _flatRegistrationFeeWei Flat registration fee charged by default for each task. + /// @param _congestionThresholdPercentage Percentage representing the acceptable upper limit of committed gas amount relative to registry_max_gas_cap. + /// Beyond this threshold, congestion fees apply. + /// @param _congestionBaseFeeWeiPerSec Base fee per second for the full capacity of the automation registry when the congestion threshold is exceeded. + /// @param _congestionExponent The congestion fee increases exponentially based on this value, ensuring higher fees as the registry approaches full capacity. + /// @param _taskCapacity Maximum number of tasks that the registry can hold. + /// @param _cycleDurationSecs Automation cycle duration in seconds. + /// @param _sysTaskDurationCapSecs Maximum allowable duration (in seconds) from the registration time that a system automation task can run. + /// @param _sysRegistryMaxGasCap Maximum gas allocation for system automation tasks per cycle. + /// @param _sysTaskCapacity Maximum number of system tasks that the registry can hold. + /// @param _vmSigner Address for the VM Signer. + /// @param _erc20Supra Address of the ERC20Supra contract. + function initialize( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint8 _congestionThresholdPercentage, + uint128 _congestionBaseFeeWeiPerSec, + uint8 _congestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity, + address _vmSigner, + address _erc20Supra + ) public initializer { + validateConfigParameters( + _taskDurationCapSecs, + _registryMaxGasCap, + _congestionThresholdPercentage, + _congestionExponent, + _taskCapacity, + _cycleDurationSecs, + _sysTaskDurationCapSecs, + _sysRegistryMaxGasCap, + _sysTaskCapacity + ); + if(_vmSigner == address(0)) revert AddressCannotBeZero(); + _erc20Supra.validateContractAddress(); + + + LibConfig.Config memory config = LibConfig.createConfig( + _registryMaxGasCap, + _sysRegistryMaxGasCap, + _automationBaseFeeWeiPerSec, + _flatRegistrationFeeWei, + _congestionBaseFeeWeiPerSec, + _taskDurationCapSecs, + _sysTaskDurationCapSecs, + _cycleDurationSecs, + _taskCapacity, + _sysTaskCapacity, + _congestionThresholdPercentage, + _congestionExponent + ); + + regConfig = LibConfig.createRegistryConfig( + _registryMaxGasCap, + _sysRegistryMaxGasCap, + true, + _vmSigner, + _erc20Supra, + config + ); + + __Ownable2Step_init(); + __Ownable_init(msg.sender); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: HELPER FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function to validate the registry configuration parameters. + function validateConfigParameters( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint8 _congestionThresholdPercentage, + uint8 _congestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) private pure { + if(_taskDurationCapSecs <= _cycleDurationSecs) { revert InvalidTaskDuration(); } + if(_registryMaxGasCap == 0) { revert InvalidRegistryMaxGasCap(); } + if(_congestionThresholdPercentage > 100) { revert InvalidCongestionThreshold(); } + if(_congestionExponent == 0) { revert InvalidCongestionExponent(); } + if(_taskCapacity == 0) { revert InvalidTaskCapacity(); } + if(_cycleDurationSecs == 0) { revert InvalidCycleDuration(); } + if(_sysTaskDurationCapSecs <= _cycleDurationSecs) { revert InvalidSysTaskDuration(); } + if(_sysRegistryMaxGasCap == 0) { revert InvalidSysRegistryMaxGasCap(); } + if(_sysTaskCapacity == 0) { revert InvalidSysTaskCapacity(); } + } + + /// @notice Helper function to validate the task duration. + function validateTaskDuration( + uint64 _regTime, + uint64 _expiryTime, + uint64 _taskDurationCap, + uint64 _cycleEndTime + ) private pure { + if(_expiryTime <= _regTime) { revert InvalidExpiryTime(); } + + uint64 taskDuration = _expiryTime - _regTime; + if(taskDuration > _taskDurationCap) { revert InvalidTaskDuration(); } + + if( _expiryTime <= _cycleEndTime) { revert TaskExpiresBeforeNextCycle(); } + } + + /// @notice Helper function to validate the inputs while registering a task. + function validateInputs(bytes memory _payloadTx, uint128 _maxGasAmount) private view { + ( , address payloadTarget, , ) = abi.decode(_payloadTx, (uint128, address, bytes, LibConfig.AccessListEntry[])); + payloadTarget.validateContractAddress(); + + if(_maxGasAmount == 0) { revert InvalidMaxGasAmount(); } + } + + /// @notice Function to ensure that AutomationController contract is the caller. + function onlyController() private view { + if(msg.sender != regConfig.automationController()) { revert CallerNotController(); } + } + + /// @notice Function to ensure that AutomationRegistry contract is the caller. + function onlyRegistry() private view { + if(msg.sender != regConfig.registry) { revert CallerNotRegistry(); } + } + + /// @notice Helper function to charge fees from the user. + function chargeFees(address _from, uint256 _amount) external { + if (msg.sender != regConfig.automationController() && msg.sender != regConfig.registry) { revert UnauthorizedCaller(); } + + bool sent = IERC20(regConfig.erc20Supra).transferFrom(_from, address(this), _amount); + if(!sent) { revert TransferFailed(); } + } + + /// @notice Function to calculate the automation congestion fee. + /// @param _totalCommittedGas Total committed gas. + /// @param _registryMaxGasCap Registry max gas cap. + /// @return Returns the automation congestion fee. + function calculateAutomationCongestionFee( + uint128 _totalCommittedGas, + uint128 _registryMaxGasCap + ) private view returns (uint128) { + if (regConfig.congestionThresholdPercentage() == 100 || regConfig.congestionBaseFeeWeiPerSec() == 0) { return 0; } + + // thresholdUsage = (totalCommittedGas / maxGasCap) * 100 + uint256 thresholdUsageScaled = (uint256(_totalCommittedGas) * DECIMAL * 100) / uint256(_registryMaxGasCap); + + uint256 thresholdPercentageScaled = uint256(regConfig.congestionThresholdPercentage()) * DECIMAL; + + // If usage is below threshold → no congestion fee + if (thresholdUsageScaled <= thresholdPercentageScaled) { + return 0; + } else { + // Calculate how much usage exceeds threshold + uint256 surplusScaled = (thresholdUsageScaled - thresholdPercentageScaled) / 100; + + + // Ensure threshold + threshold surplus does not exceed 1 (1 in scaled terms) + uint256 thresholdScaledAsFraction = thresholdPercentageScaled / 100; // DECIMAL-scaled fraction + uint256 surplusClipped = thresholdScaledAsFraction + surplusScaled > DECIMAL ? DECIMAL - thresholdScaledAsFraction : surplusScaled; + + uint256 baseScaled = DECIMAL + surplusClipped; // (1 + base) + uint256 resultScaled = DECIMAL; + for (uint8 i = 0; i < regConfig.congestionExponent(); i++) { + resultScaled = (resultScaled * baseScaled) / DECIMAL; + } + uint256 exponentResult = resultScaled - DECIMAL; // subtract 1 + + + // Multiply base fee (wei/sec) with exponentResult and downscale by DECIMAL + uint256 acf = (uint256(regConfig.congestionBaseFeeWeiPerSec()) * exponentResult) / DECIMAL; + + return uint128(acf); + } + } + + /// @notice Calculates the automation fee multiplier for cycle. + /// @param _totalCommittedGas Total committed gas. + /// @param _registryMaxGasCap Registry max gas cap. + function calculateAutomationFeeMultiplierForCycle( + uint128 _totalCommittedGas, + uint128 _registryMaxGasCap + ) private view returns (uint128) { + uint128 congesionFee = calculateAutomationCongestionFee(_totalCommittedGas, _registryMaxGasCap); + return (congesionFee + regConfig.automationBaseFeeWeiPerSec()); + } + + /// @notice Calculates automation task fees for a single task at the time of new cycle. + /// This is supposed to be called only after removing expired task and must not be called for expired task. + function calculateAutomationFeeForInterval( + uint64 _duration, + uint128 _taskOccupancy, + uint128 _automationFeePerSec, + uint128 _registryMaxGasCap + ) private pure returns (uint128) { + uint256 taskOccupancyRatioByDuration = (uint256(_duration) * uint256(_taskOccupancy) * DECIMAL) / uint256(_registryMaxGasCap); + + uint256 automationFeeForInterval = _automationFeePerSec * taskOccupancyRatioByDuration; + + return uint128(automationFeeForInterval / DECIMAL); + } + + /// @notice Calculates automation task fees for a single task at the time of new cycle. + /// This is supposed to be called only after removing expired task and must not be called for expired task. + /// @param _state State of the task. + /// @param _expiryTime Task expiry time. + /// @param _maxGasAmount Task's max gas amount + /// @param _potentialFeeTimeframe Potential time frame to calculate task fees for. + /// @param _currentTime Current time + /// @param _automationFeePerSec Automation fee per sec + /// @return Calculated task fee for the interval the task will be active. + function _calculateTaskFee( + CommonUtils.TaskState _state, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _potentialFeeTimeframe, + uint64 _currentTime, + uint128 _automationFeePerSec + ) private view returns (uint128) { + if (_automationFeePerSec == 0) { return 0; } + if (_expiryTime <= _currentTime) { return 0; } + + uint64 taskActiveTimeframe = _expiryTime - _currentTime; + + // If the task is a new task i.e. in Pending state, then it is charged always for + // the input _potentialFeeTimeframe(which is cycle-interval), + // For the new tasks which active-timeframe is less than cycle-interval + // it would mean it is their first and only cycle and we charge the fee for entire cycle. + // Note that although the new short tasks are charged for entire cycle, the refunding logic remains the same for + // them as for the long tasks. + // This way bad-actors will be discourged to submit small and short tasks with big occupancy by blocking other + // good-actors register tasks. + uint64 actualFeeTimeframe; + if(_state == CommonUtils.TaskState.PENDING) { + actualFeeTimeframe = _potentialFeeTimeframe; + } else { + actualFeeTimeframe = taskActiveTimeframe < _potentialFeeTimeframe ? taskActiveTimeframe : _potentialFeeTimeframe; + } + return calculateAutomationFeeForInterval( + actualFeeTimeframe, + _maxGasAmount, + _automationFeePerSec, + regConfig.registryMaxGasCap() + ); + } + + /// @notice Estimates automation fee the next cycle for specified task occupancy for the configured cycle interval + /// referencing the current automation registry fee parameters, specified total/committed occupancy and registry + /// maximum allowed occupancy for the next cycle. + /// Note it is expected that committed_occupancy does not include current task's occupancy. + function estimateAutomationFeeWithCommittedOccupancyInternal( + uint128 _taskOccupancy, + uint128 _committedOccupancy + ) private view returns (uint128) { + uint128 totalCommittedGas = _taskOccupancy + _committedOccupancy; + + uint128 automationFeePerSec = calculateAutomationFeeMultiplierForCycle(totalCommittedGas, regConfig.nextCycleRegistryMaxGasCap()); + + if(automationFeePerSec == 0) return 0; + + uint64 durationSecs = IAutomationController(regConfig.automationController()).getCycleDuration(); + return calculateAutomationFeeForInterval(durationSecs, _taskOccupancy, automationFeePerSec, regConfig.nextCycleRegistryMaxGasCap()); + } + + /// @notice Unlocks the deposit paid by the task from the total automation fees deposited. + /// @dev Error event is emitted if the total automation fees deposited is less than the requested unlock amount. + /// @param _taskIndex Index of the task. + /// @param _lockedDeposit Locked deposit amount to be unlocked. + /// @return Bool if _lockedDeposit can be unlocked safely. + function _safeUnlockLockedDeposit( + uint64 _taskIndex, + uint128 _lockedDeposit + ) private returns (bool) { + uint256 totalDeposited = regConfig.totalDepositedAutomationFees; + + if(totalDeposited >= _lockedDeposit) { + regConfig.totalDepositedAutomationFees = totalDeposited - _lockedDeposit; + return true; + } + + emit ErrorUnlockTaskDepositFee(_taskIndex, totalDeposited, _lockedDeposit); + return false; + } + + /// @notice Helper function to transfer refunds. + /// @param _to Recipeint of the refund + /// @param _amount Amount to refund + /// @return Bool representing if refund was successful. + function _refund(address _to, uint128 _amount) private returns (bool) { + bool sent = IERC20(regConfig.erc20Supra).transfer(_to, _amount); + if (!sent) { revert TransferFailed(); } + + return sent; + } + + /// @notice Refunds the specified amount to the task owner. + /// @dev Error event is emitted if the registry contract does not have sufficient balance. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableAmount Amount to refund. + /// @param _refundType Type of refund. + /// @return Bool representing if refund was successful. + function safeRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableAmount, + uint8 _refundType + ) private returns (bool) { + uint256 balance = IERC20(regConfig.erc20Supra).balanceOf(address(this)); + if(balance < _refundableAmount) { + emit ErrorInsufficientBalanceToRefund(_taskIndex, _taskOwner, _refundType, _refundableAmount); + return false; + } else { + return _refund(_taskOwner, _refundableAmount); + } + } + + /// @notice Refunds the specified amount of deposit to the task owner and unlocks full deposit from the total automation fees deposited. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableDeposit Refundable amount of deposit. + /// @param _lockedDeposit Total locked deposit. + function _safeDepositRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) private returns (bool) { + // Ensures that amount to unlock is not more than the total automation fees deposited. + bool result = _safeUnlockLockedDeposit(_taskIndex, _lockedDeposit); + if (!result) { + return result; + } + + result = safeRefund(_taskIndex, _taskOwner, _refundableDeposit, DEPOSIT_CYCLE_FEE); + + if (result) { emit TaskDepositFeeRefund(_taskIndex, _taskOwner, _refundableDeposit); } + return result; + } + + /// @notice Unlocks the locked fee paid by the task for cycle. + /// Error event is emitted if the cycle locked fee amount is inconsistent with the requested unlock amount. + /// @param _cycleLockedFees Locked cycle fees + /// @param _refundableFee Refundable fees + /// @param _taskIndex Index of the task + /// @return Bool if _refundableFee can be unlocked safely. + /// @return Updated _cycleLockedFees after unlocking _refundableFee. + function safeUnlockLockedCycleFee( + uint256 _cycleLockedFees, + uint64 _refundableFee, + uint64 _taskIndex + ) private returns (bool, uint256) { + // This check makes sure that more than locked amount of the fees will be not be refunded. + // Any attempt means internal bug. + bool hasLockedFee = _cycleLockedFees >= _refundableFee; + if (hasLockedFee) { + // Unlock the refunded amount + _cycleLockedFees = _cycleLockedFees - _refundableFee; + } else { + emit ErrorUnlockTaskCycleFee(_taskIndex, _cycleLockedFees, _refundableFee); + } + return (hasLockedFee, _cycleLockedFees); + } + + /// @notice Refunds fee paid by the task for the cycle to the task owner. + /// Note that here we do not unlock the fee, as on cycle change locked cycle-fees for the ended cycle are + /// automatically unlocked. + function safeFeeRefund( + uint64 _taskIndex, + address _taskOwner, + uint256 _cycleLockedFees, + uint64 _refundableFee + ) private returns (bool, uint256) { + bool result; + uint256 remainingLockedFees; + + (result, remainingLockedFees) = safeUnlockLockedCycleFee(_cycleLockedFees, _refundableFee, _taskIndex); + if (!result) { return (result, remainingLockedFees); } + + result = safeRefund( _taskIndex, _taskOwner, _refundableFee, CYCLE_FEE); + if (result) { emit TaskFeeRefund(_taskIndex, _taskOwner, _refundableFee); } + return (result, remainingLockedFees); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CONTROLLER FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function to update the registry configuration, reverts if caller is not AutomationController. + function applyPendingConfig() external returns (bool, uint64) { + onlyController(); + + if (!configBuffer.ifExists) { + return (false, 0); + } + uint64 pendingCycleDuration = configBuffer.pendingConfig.cycleDurationSecs(); + regConfig.config = configBuffer.pendingConfig; + + delete configBuffer; + + return (true, pendingCycleDuration); + } + + /// @notice Internally calls _calculateTaskFee. + function calculateTaskFee( + CommonUtils.TaskState _state, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _potentialFeeTimeframe, + uint64 _currentTime, + uint128 _automationFeePerSec + ) external view returns (uint128) { + return _calculateTaskFee( + _state, + _expiryTime, + _maxGasAmount, + _potentialFeeTimeframe, + _currentTime, + _automationFeePerSec + ); + } + + /// @notice Internally calls _safeUnlockLockedDeposit, reverts if caller is not AutomationController. + function safeUnlockLockedDeposit( + uint64 _taskIndex, + uint128 _lockedDeposit + ) external returns (bool) { + onlyController(); + + return _safeUnlockLockedDeposit(_taskIndex, _lockedDeposit); + } + + /// @notice Refunds the deposit fee and any autoamtion fees of the task. + function refundTaskFees( + uint64 _currentTime, + uint64 _refundDuration, + uint128 _automationFeePerSec, + CommonUtils.TaskDetails memory _task + ) external { + onlyController(); + + // Do not attempt fee refund if remaining duration is 0 + if (_task.state != CommonUtils.TaskState.PENDING && _refundDuration != 0) { + uint128 _refundFee = _calculateTaskFee( + _task.state, + _task.expiryTime, + _task.maxGasAmount, + _refundDuration, + _currentTime, + _automationFeePerSec + ); + ( , uint256 remainingCycleLockedFees) = safeFeeRefund( + _task.taskIndex, + _task.owner, + regConfig.cycleLockedFees, + uint64(_refundFee) + ); + regConfig.cycleLockedFees = remainingCycleLockedFees; + } + + _safeDepositRefund( + _task.taskIndex, + _task.owner, + _task.depositFee, + _task.depositFee + ); + } + + function calculateAutomationFeeMultiplierForCurrentCycleInternal() external view returns (uint128) { + // Compute the automation fee multiplier for this cycle + return calculateAutomationFeeMultiplierForCycle( + regConfig.gasCommittedForThisCycle(), + regConfig.registryMaxGasCap() + ); + } + + /// @notice Calculates automation fee per second for the specified task occupancy + /// referencing the current automation registry fee parameters, specified total/committed occupancy and current registry + /// maximum allowed occupancy. + function calculateAutomationFeeMultiplierForCommittedOccupancy( + uint128 _totalCommittedMaxGas + ) external view returns (uint128) { + // Compute the automation fee multiplier for cycle + return calculateAutomationFeeMultiplierForCycle( + _totalCommittedMaxGas, + regConfig.registryMaxGasCap() + ); + } + + /// @notice Function to update the cycle locked fees and gas committed. + /// @param _lockedFees Updated cycle locked fees + /// @param _sysGasCommittedForNextCycle Updated system gas committed for next cycle + /// @param _gasCommittedForNextCycle Updated gas committed for next cycle + /// @param _gasCommittedForNewCycle Updated gas committed for new cycle + function updateGasCommittedAndCycleLockedFees( + uint256 _lockedFees, + uint128 _sysGasCommittedForNextCycle, + uint128 _gasCommittedForNextCycle, + uint128 _gasCommittedForNewCycle + ) external { + onlyController(); + + regConfig.cycleLockedFees = _lockedFees; + regConfig.setSysGasCommittedForNextCycle(_sysGasCommittedForNextCycle); + regConfig.setSysGasCommittedForThisCycle(_sysGasCommittedForNextCycle); + regConfig.setGasCommittedForNextCycle(_gasCommittedForNextCycle); + regConfig.setGasCommittedForThisCycle(_gasCommittedForNewCycle); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: REGISTRY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that performs validation and updates state for a valid task. + function updateStateForValidRegistration( + uint256 _totalTasks, + uint64 _regTime, + uint64 _expiryTime, + CommonUtils.TaskType _taskType, + bytes memory _payloadTx, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle + ) external { + onlyRegistry(); + + // Check if automation and registration is enabled + IAutomationController automationController = IAutomationController(regConfig.automationController()); + if (!automationController.isAutomationEnabled()) { revert AutomationNotEnabled(); } + if (!regConfig.registrationEnabled()) { revert RegistrationDisabled(); } + + if (!automationController.isCycleStarted()) { revert CycleTransitionInProgress(); } + + bool isUST = _taskType == CommonUtils.TaskType.UST; + + uint64 taskDurationCap; + uint128 gasCommittedForNextCycle; + uint128 nextCycleRegistryMaxGasCap; + if (isUST) { + if(_totalTasks >= regConfig.taskCapacity()) { revert TaskCapacityReached(); } + if(_gasPriceCap == 0) { revert InvalidGasPriceCap(); } + + gasCommittedForNextCycle = regConfig.gasCommittedForNextCycle(); + uint128 estimatedAutomationFeeForCycle = estimateAutomationFeeWithCommittedOccupancyInternal(_maxGasAmount, gasCommittedForNextCycle); + if(_automationFeeCapForCycle < estimatedAutomationFeeForCycle) { revert InsufficientFeeCapForCycle(); } + + taskDurationCap = regConfig.taskDurationCapSecs(); + nextCycleRegistryMaxGasCap = regConfig.nextCycleRegistryMaxGasCap(); + } else { + if(_totalTasks >= regConfig.sysTaskCapacity()) { revert TaskCapacityReached(); } + + gasCommittedForNextCycle = regConfig.sysGasCommittedForNextCycle(); + taskDurationCap = regConfig.sysTaskDurationCapSecs(); + nextCycleRegistryMaxGasCap = regConfig.nextCycleSysRegistryMaxGasCap(); + } + + validateTaskDuration(_regTime, _expiryTime, taskDurationCap, automationController.getCycleEndTime()); + validateInputs(_payloadTx, _maxGasAmount); + + uint128 gasCommitted = _maxGasAmount + gasCommittedForNextCycle; + if(gasCommitted > nextCycleRegistryMaxGasCap) { revert GasCommittedExceedsMaxGasCap(); } + + if (isUST) { + regConfig.setGasCommittedForNextCycle(gasCommitted); + } else { + regConfig.setSysGasCommittedForNextCycle(gasCommitted); + } + } + + function updateGasCommittedForNextCycle(CommonUtils.TaskType _taskType, uint128 _maxGasAmount) external { + onlyRegistry(); + + bool isUST = _taskType == CommonUtils.TaskType.UST; + + uint128 gasCommittedForNextCycle = isUST ? regConfig.gasCommittedForNextCycle(): regConfig.sysGasCommittedForNextCycle(); + if (gasCommittedForNextCycle < _maxGasAmount) { revert GasCommittedValueUnderflow(); } + + // Adjust the gas committed for the next cycle by subtracting the gas amount of the cancelled/stopped task + if (isUST) { + regConfig.setGasCommittedForNextCycle(gasCommittedForNextCycle - _maxGasAmount); + } else { + regConfig.setSysGasCommittedForNextCycle(gasCommittedForNextCycle - _maxGasAmount); + } + } + + /// @notice Helper function to increment the total deposited automation fees. + function incTotalDepositedAutomationFees(uint256 _amount) external { + onlyRegistry(); + regConfig.totalDepositedAutomationFees += _amount; + } + + /// @notice Internally calls _refund, reverts if caller is not AutomationRegistry. + function refund(address _to, uint128 _amount) external { + onlyRegistry(); + uint256 balance = IERC20(regConfig.erc20Supra).balanceOf(address(this)); + + if(balance < _amount) { revert InsufficientBalanceForRefund(); } + _refund(_to, _amount); + } + + /// @notice Internally calls _safeDepositRefund, reverts if caller is not AutomationRegistry. + function safeDepositRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) external returns (bool) { + onlyRegistry(); + return _safeDepositRefund(_taskIndex, _taskOwner, _refundableDeposit, _lockedDeposit); + } + + /// @notice Helper function to unlock locked deposit and cycle fees when stopTasks is called. + function unlockDepositAndCycleFee( + uint64 _taskIndex, + CommonUtils.TaskState _taskState, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _residualInterval, + uint64 _currentTime, + uint128 _depositFee + ) external returns (uint128, uint128) { + onlyRegistry(); + + uint128 cycleFeeRefund; + uint128 depositRefund; + + if(_taskState != CommonUtils.TaskState.PENDING) { + // Compute the automation fee multiplier for cycle + uint128 automationFeePerSec = calculateAutomationFeeMultiplierForCycle(regConfig.gasCommittedForThisCycle(), regConfig.registryMaxGasCap()); + + uint128 taskFee = _calculateTaskFee( + _taskState, + _expiryTime, + _maxGasAmount, + _residualInterval, + _currentTime, + automationFeePerSec + ); + + // Refund full deposit and the half of the remaining run-time fee when task is active or cancelled stage + cycleFeeRefund = taskFee / REFUND_FRACTION; + depositRefund = _depositFee; + } else { + cycleFeeRefund = 0; + depositRefund = _depositFee / REFUND_FRACTION; + } + + bool result = _safeUnlockLockedDeposit(_taskIndex, _depositFee); + if(!result) { revert ErrorDepositRefund(); } + + (bool hasLockedFee, uint256 remainingCycleLockedFees ) = safeUnlockLockedCycleFee(regConfig.cycleLockedFees, uint64(cycleFeeRefund), _taskIndex); + if(!hasLockedFee) { revert ErrorCycleFeeRefund(); } + + regConfig.cycleLockedFees = remainingCycleLockedFees; + + return (cycleFeeRefund, depositRefund); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ADMIN FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function to update the registry configuration buffer. + function updateConfigBuffer( + uint64 _taskDurationCapSecs, + uint128 _registryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint8 _congestionThresholdPercentage, + uint128 _congestionBaseFeeWeiPerSec, + uint8 _congestionExponent, + uint16 _taskCapacity, + uint64 _cycleDurationSecs, + uint64 _sysTaskDurationCapSecs, + uint128 _sysRegistryMaxGasCap, + uint16 _sysTaskCapacity + ) external onlyOwner { + validateConfigParameters( + _taskDurationCapSecs, + _registryMaxGasCap, + _congestionThresholdPercentage, + _congestionExponent, + _taskCapacity, + _cycleDurationSecs, + _sysTaskDurationCapSecs, + _sysRegistryMaxGasCap, + _sysTaskCapacity + ); + + if(regConfig.gasCommittedForNextCycle() > _registryMaxGasCap) { revert UnacceptableRegistryMaxGasCap(); } + if(regConfig.sysGasCommittedForNextCycle() > _sysRegistryMaxGasCap) { revert UnacceptableSysRegistryMaxGasCap(); } + + // Add new config to the buffer + LibConfig.Config memory pendingConfig = LibConfig.createConfig( + _registryMaxGasCap, + _sysRegistryMaxGasCap, + _automationBaseFeeWeiPerSec, + _flatRegistrationFeeWei, + _congestionBaseFeeWeiPerSec, + _taskDurationCapSecs, + _sysTaskDurationCapSecs, + _cycleDurationSecs, + _taskCapacity, + _sysTaskCapacity, + _congestionThresholdPercentage, + _congestionExponent + ); + configBuffer = LibConfig.ConfigBuffer(pendingConfig, true); + + regConfig.setNextCycleRegistryMaxGasCap(_registryMaxGasCap); + regConfig.setNextCycleSysRegistryMaxGasCap(_sysRegistryMaxGasCap); + + emit ConfigBufferUpdated(pendingConfig.getConfig()); + } + + /// @notice Function to enable the task registration. + function enableRegistration() external onlyOwner { + if(regConfig.registrationEnabled()) { revert AlreadyEnabled(); } + regConfig.setRegistrationEnabled(true); + + emit TaskRegistrationEnabled(regConfig.registrationEnabled()); + } + + /// @notice Function to disable the task registration. + function disableRegistration() external onlyOwner { + if(!regConfig.registrationEnabled()) { revert AlreadyDisabled(); } + regConfig.setRegistrationEnabled(false); + + emit TaskRegistrationDisabled(regConfig.registrationEnabled()); + } + + /// @notice Function to update the VM Signer address. + /// @param _vmSigner New address for VM Signer. + function setVmSigner(address _vmSigner) external onlyOwner { + if(_vmSigner == address(0)) { revert AddressCannotBeZero(); } + + address oldVmSigner = regConfig.vmSigner; + regConfig.vmSigner = _vmSigner; + + emit VmSignerUpdated(oldVmSigner, _vmSigner); + } + + /// @notice Function to update the ERC20Supra address. + /// @param _erc20Supra New address for ERC20Supra. + function setErc20Supra(address _erc20Supra) external onlyOwner { + _erc20Supra.validateContractAddress(); + + address oldErc20Supra = regConfig.erc20Supra; + regConfig.erc20Supra = _erc20Supra; + + emit Erc20SupraUpdated(oldErc20Supra, _erc20Supra); + } + + /// @notice Function to update the automation controller smart contract address. + /// @param _controller Address of the automation controller smart contact. + function setAutomationController(address _controller) external onlyOwner { + _controller.validateContractAddress(); + + address oldController = regConfig.automationController(); + regConfig.setAutomationController(_controller); + + emit AutomationControllerUpdated(oldController, _controller); + } + + /// @notice Function to update the automation registry smart contract address. + /// @param _registry Address of the automation registry smart contact. + function setAutomationRegistry(address _registry) external onlyOwner { + _registry.validateContractAddress(); + + address oldRegistry = regConfig.registry; + regConfig.registry = _registry; + + emit AutomationRegistryUpdated(oldRegistry, _registry); + } + + /// @notice Function to withdraw the accumulated fees. + /// @param _amount Amount to withdraw. + /// @param _recipient Address to withdraw fees to. + function withdrawFees(uint256 _amount, address _recipient) external onlyOwner { + if(_amount == 0) { revert InvalidAmount(); } + if(_recipient == address(0)) { revert AddressCannotBeZero(); } + uint256 balance = IERC20(regConfig.erc20Supra).balanceOf(address(this)); + + if(balance < _amount) { revert InsufficientBalance(); } + if(balance - _amount < regConfig.cycleLockedFees + regConfig.totalDepositedAutomationFees) { revert RequestExceedsLockedBalance(); } + + bool sent = IERC20(regConfig.erc20Supra).transfer(_recipient, _amount); + if(!sent) { revert TransferFailed(); } + + emit RegistryFeeWithdrawn(_recipient, _amount); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Returns the VM Signer address. + function getVmSigner() external view returns (address) { + return regConfig.vmSigner; + } + + /// @notice Returns the ERC20Supra address. + function erc20Supra() external view returns (address) { + return regConfig.erc20Supra; + } + + /// @notice Returns the address of AutomationController smart contract. + function getAutomationController() external view returns (address) { + return regConfig.automationController(); + } + + /// @notice Returns the address of AutomationRegistry smart contract. + function getAutomationRegistry() external view returns (address) { + return regConfig.registry; + } + + /// @notice Returns if task registration is enabled. + function isRegistrationEnabled() external view returns (bool) { + return regConfig.registrationEnabled(); + } + + /// @notice Returns the gas committed for the next cycle. + function getGasCommittedForNextCycle() external view returns (uint128) { + return regConfig.gasCommittedForNextCycle(); + } + + /// @notice Returns the gas committed for the current cycle. + function getGasCommittedForCurrentCycle() external view returns (uint128) { + return regConfig.gasCommittedForThisCycle(); + } + + /// @notice Returns the system gas committed for the next cycle. + function getSystemGasCommittedForNextCycle() external view returns (uint128) { + return regConfig.sysGasCommittedForNextCycle(); + } + + /// @notice Returns the system gas committed for the current cycle. + function getSystemGasCommittedForCurrentCycle() external view returns (uint128) { + return regConfig.sysGasCommittedForThisCycle(); + } + + /// @notice Returns the registry max gas cap for the next cycle. + function getNextCycleRegistryMaxGasCap() external view returns (uint128) { + return regConfig.nextCycleRegistryMaxGasCap(); + } + + /// @notice Returns the system registry max gas cap for the next cycle. + function getNextCycleSysRegistryMaxGasCap() external view returns (uint128) { + return regConfig.nextCycleSysRegistryMaxGasCap(); + } + + /// @notice Returns the flat registration fee. + function flatRegistrationFeeWei() external view returns (uint128) { + return regConfig.flatRegistrationFeeWei(); + } + + /// @notice Returns the registry configuration. + function getConfig() external view returns (LibConfig.ConfigDetails memory) { + return regConfig.config.getConfig(); + } + + /// @notice Returns the pending configuration. + function getPendingConfig() external view returns (LibConfig.ConfigDetails memory) { + return configBuffer.pendingConfig.getConfig(); + } + + /// @notice Returns the registry max gas cap configured. + function getRegistryMaxGasCap() external view returns (uint128) { + return regConfig.registryMaxGasCap(); + } + + /// @notice Returns the system registry max gas cap configured. + function getSysRegistryMaxGasCap() external view returns (uint128) { + return regConfig.sysRegistryMaxGasCap(); + } + + /// @notice Returns the automationBaseFeeWeiPerSec configured. + function getAutomationBaseFeeWeiPerSec() external view returns (uint128) { + return regConfig.automationBaseFeeWeiPerSec(); + } + + /// @notice Returns the cycle duration configured. + function cycleDurationSecs() external view returns (uint64) { + return regConfig.config.cycleDurationSecs(); + } + + /// @notice Returns the locked fees for the cycle. + function getCycleLockedFees() external view returns (uint256) { + return regConfig.cycleLockedFees; + } + + /// @notice Returns the total amount of automation fees deposited. + function getTotalDepositedAutomationFees() external view returns (uint256) { + return regConfig.totalDepositedAutomationFees; + } + + /// @notice Returns the total amount locked which comprises of 'cycleLockedFees' and 'totalDepositedAutomationFees'. + function getTotalLockedBalance() external view returns (uint256) { + return regConfig.cycleLockedFees + regConfig.totalDepositedAutomationFees; + } + + /// @notice Estimates automation fee for the next cycle for specified task occupancy for the configured cycle-interval + /// referencing the current automation registry fee parameters, current total occupancy and registry maximum allowed + /// occupancy for the next cycle. + function estimateAutomationFee(uint128 _taskOccupancy) external view returns (uint128) { + return estimateAutomationFeeWithCommittedOccupancyInternal(_taskOccupancy, regConfig.gasCommittedForNextCycle()); + } + + /// @notice Estimates automation fee the next cycle for specified task occupancy for the configured cycle-interval + /// referencing the current automation registry fee parameters, specified total/committed occupancy and registry + /// maximum allowed occupancy for the next cycle. + function estimateAutomationFeeWithCommittedOccupancy( + uint128 _taskOccupancy, + uint128 _committedOccupancy + ) external view returns (uint128) { + return estimateAutomationFeeWithCommittedOccupancyInternal( + _taskOccupancy, + _committedOccupancy + ); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/AutomationRegistry.sol b/solidity/supra_contracts/src/AutomationRegistry.sol new file mode 100644 index 0000000000..cbd6071532 --- /dev/null +++ b/solidity/supra_contracts/src/AutomationRegistry.sol @@ -0,0 +1,699 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {EnumerableSet} from "../lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {CommonUtils} from "./CommonUtils.sol"; +import {LibRegistry} from "./LibRegistry.sol"; + +import {IAutomationCore} from "./IAutomationCore.sol"; +import {IAutomationController} from "./IAutomationController.sol"; +import {IAutomationRegistry} from "./IAutomationRegistry.sol"; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Ownable2StepUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "../lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol"; + +contract AutomationRegistry is IAutomationRegistry, Ownable2StepUpgradeable, UUPSUpgradeable { + using EnumerableSet for *; + using CommonUtils for *; + using LibRegistry for *; + + /// @dev Defines divisor for refunds of deposit fees with penalty + /// Factor of `2` suggests that `1/2` of the deposit will be refunded. + uint8 constant REFUND_FACTOR = 2; + + /// @notice Address of the transaction hash precompile. + address public constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + /// @dev State variables + LibRegistry.RegistryState regState; + address public automationCore; + address public automationController; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Emitted when a user task is registered. + event TaskRegistered( + uint64 indexed taskIndex, + address indexed owner, + uint128 registrationFee, + uint128 lockedDepositFee, + CommonUtils.TaskDetails taskMetadata + ); + + /// @notice Emitted when a system task is registered. + event SystemTaskRegistered( + uint64 indexed taskIndex, + address indexed owner, + uint256 timestamp, + CommonUtils.TaskDetails taskMetadata + ); + + /// @notice Emitted when an account is authorized as submitter for system tasks. + event AuthorizationGranted(address indexed account, uint256 indexed timestamp); + + /// @notice Emitted when authorization is revoked for an account to submit system tasks. + event AuthorizationRevoked(address indexed account, uint256 indexed timestamp); + + /// @notice Emitted when the AutomationCore contract address is updated. + event AutomationCoreUpdated(address indexed oldAutomationCore, address indexed newAutomationCore); + + /// @notice Emitted when the AutomationController contract address is updated. + event AutomationControllerUpdated(address indexed oldAutomationController, address indexed newAutomationController); + + /// @notice Emitted when a task is cancelled. + event TaskCancelled( + uint64 indexed taskIndex, + address indexed owner, + bytes32 indexed regHash + ); + + /// @notice Emitted when a task is stopped. + event TasksStopped( + LibRegistry.TaskStopped[] indexed stoppedTasks, + address indexed owner + ); + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CONSTRUCTOR AND INITIALIZER :::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Disables the initialization for the implementation contract. + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the owner and AutomationCore contract address, can only be called once. + /// @param _automationCore Address of the AutomationCore contract. + function initialize(address _automationCore) public initializer { + _automationCore.validateContractAddress(); + + automationCore = _automationCore; + + __Ownable2Step_init(); + __Ownable_init(msg.sender); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: TASKS RELATED FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Function used to register a user task for automation. + /// @param _payloadTx Includes the target smart contract address and the data to call in abi encoded form. + /// @param _expiryTime Time after which the task gets expired. + /// @param _maxGasAmount Maximum amount of gas for the automation task. + /// @param _gasPriceCap Maximum gas willing to pay for the task. + /// @param _automationFeeCapForCycle Maximum automation fee for a cycle to be paid ever. + /// @param _priority Priority for the task. 0 for default priority. + /// @param _auxData Auxiliary data to be passed. + function register( + bytes memory _payloadTx, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint64 _priority, + bytes[] memory _auxData + ) external { + uint64 regTime = uint64(block.timestamp); + + IAutomationCore core = IAutomationCore(automationCore); + core.updateStateForValidRegistration( + totalTasks(), + regTime, + _expiryTime, + CommonUtils.TaskType.UST, + _payloadTx, + _maxGasAmount, + _gasPriceCap, + _automationFeeCapForCycle + ); + + uint64 taskIndex = regState.currentIndex; + + LibRegistry.TaskMetadata memory taskMetadata = LibRegistry.createTaskMetadata( + _maxGasAmount, + _gasPriceCap, + _automationFeeCapForCycle, + _automationFeeCapForCycle , + readTxHash(), + taskIndex, + regTime, + _expiryTime, + taskIndex, // priority set to taskIndex + msg.sender, + CommonUtils.TaskType.UST, + CommonUtils.TaskState.PENDING, + _payloadTx, + _auxData + ); + + regState.tasks[taskIndex] = taskMetadata; + require(regState.taskIdList.add(taskIndex), TaskIndexNotUnique()); + regState.currentIndex += 1; + + core.incTotalDepositedAutomationFees(_automationFeeCapForCycle); + uint128 flatRegistrationFeeWei = core.flatRegistrationFeeWei(); + uint128 fee = flatRegistrationFeeWei + _automationFeeCapForCycle; + core.chargeFees(msg.sender, fee); + + emit TaskRegistered(taskIndex, msg.sender, flatRegistrationFeeWei, _automationFeeCapForCycle, regState.tasks[taskIndex].getTaskDetails()); + } + + /// @notice Function to register a system task. Reverts if caller is not authorized. + /// @param _payloadTx Includes the target smart contract address and the data to call in abi encoded form. + /// @param _expiryTime Time after which the task gets expired. + /// @param _maxGasAmount Maximum amount of gas for the automation task. + /// @param _priority Priority for the task. 0 for default priority. + /// @param _auxData Auxiliary data to be passed. + function registerSystemTask( + bytes memory _payloadTx, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _priority, + bytes[] memory _auxData + ) external { + if(!isAuthorizedSubmitter(msg.sender)) { revert UnauthorizedAccount(); } + + uint64 regTime = uint64(block.timestamp); + IAutomationCore(automationCore).updateStateForValidRegistration( + totalSystemTasks(), + regTime, + _expiryTime, + CommonUtils.TaskType.GST, + _payloadTx, + _maxGasAmount, + 0, + 0 + ); + + uint64 taskIndex = regState.currentIndex; + uint64 taskPriority = _priority == 0 ? taskIndex : _priority; // Defaults to taskIndex as priority if 0 is passed + LibRegistry.TaskMetadata memory taskMetadata = LibRegistry.createTaskMetadata( + _maxGasAmount, + 0, + 0, + 0, + readTxHash(), + taskIndex, + regTime, + _expiryTime, + taskPriority, + msg.sender, + CommonUtils.TaskType.GST, + CommonUtils.TaskState.PENDING, + _payloadTx, + _auxData + ); + + regState.tasks[taskIndex] = taskMetadata; + require(regState.taskIdList.add(taskIndex), TaskIndexNotUnique()); + require(regState.sysTaskIds.add(taskIndex), TaskIndexNotUnique()); + regState.currentIndex += 1; + + emit SystemTaskRegistered(taskIndex, msg.sender, block.timestamp, regState.tasks[taskIndex].getTaskDetails()); + } + + /// @notice Cancels an automation task with specified task index. + /// Only existing task, which is PENDING or ACTIVE, can be cancelled and only by task owner. + /// If the task is + /// - active, its state is updated to be CANCELLED. + /// - pending, it is removed form the list. + /// - cancelled, an error is reported + /// Committed gas limit is updated by reducing it with the max gas amount of the cancelled task. + /// @param _taskIndex Index of the task. + function cancelTask( + uint64 _taskIndex + ) external { + // Check if automation is enabled + IAutomationController controller = IAutomationController(automationController); + if (!controller.isAutomationEnabled()) { revert AutomationNotEnabled(); } + + if(!controller.isCycleStarted()) { revert CycleTransitionInProgress(); } + if(!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + + CommonUtils.TaskDetails memory task = regState.tasks[_taskIndex].getTaskDetails(); + + if(task.taskType == CommonUtils.TaskType.GST) { revert UnsupportedTaskOperation(); } + if(task.owner != msg.sender) { revert UnauthorizedAccount(); } + if(task.state == CommonUtils.TaskState.CANCELLED) { revert AlreadyCancelled(); } + + IAutomationCore core = IAutomationCore(automationCore); + if (task.state == CommonUtils.TaskState.PENDING) { + // When Pending tasks are cancelled, refund of the deposit fee is done with penalty + _removeTask(_taskIndex, false); + bool result = core.safeDepositRefund( + _taskIndex, + task.owner, + task.depositFee / REFUND_FACTOR, + task.depositFee + ); + if(!result) { revert ErrorDepositRefund(); } + } else { + // It is safe not to check the state as above, the cancelled tasks are already rejected. + // Active tasks will be refunded the deposited amount fully at the end of the cycle. + LibRegistry.setState(regState.tasks[_taskIndex], uint8(CommonUtils.TaskState.CANCELLED)); + } + + // This check means the task was expected to be executed in the next cycle, but it has been cancelled. + // We need to remove its gas commitment from `gasCommittedForNextCycle` for this particular task. + if (task.expiryTime > controller.getCycleEndTime()) { + core.updateGasCommittedForNextCycle(task.taskType, task.maxGasAmount); + } + + emit TaskCancelled( _taskIndex, task.owner, task.txHash); + } + + /// @notice Cancels a system automation task with specified task index. + /// Only existing task, which is PENDING or ACTIVE, can be cancelled and only by task owner. + /// If the task is + /// - active, its state is updated to be CANCELLED. + /// - pending, it is removed form the list. + /// - cancelled, an error is reported + /// Committed gas limit is updated by reducing it with the max gas amount of the cancelled task. + /// @param _taskIndex Index of the task. + function cancelSystemTask( + uint64 _taskIndex + ) external { + // Check if automation is enabled + IAutomationController controller = IAutomationController(automationController); + if (!controller.isAutomationEnabled()) { revert AutomationNotEnabled(); } + + if(!controller.isCycleStarted()) { revert CycleTransitionInProgress(); } + if(!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + if(!ifSysTaskExists(_taskIndex)) { revert SystemTaskDoesNotExist(); } + + CommonUtils.TaskDetails memory task = regState.tasks[_taskIndex].getTaskDetails(); + + // Check if GST + if(task.taskType == CommonUtils.TaskType.UST) { revert UnsupportedTaskOperation(); } + + if(task.owner != msg.sender) { revert UnauthorizedAccount(); } + if(task.state == CommonUtils.TaskState.CANCELLED) { revert AlreadyCancelled(); } + + if(task.state == CommonUtils.TaskState.PENDING) { + _removeTask(_taskIndex, true); + } else { + LibRegistry.setState(regState.tasks[_taskIndex], uint8(CommonUtils.TaskState.CANCELLED)); + } + + // This check means the task was expected to be executed in the next cycle, but it has been cancelled. + // We need to remove its gas commitment from `gasCommittedForNextCycle` for this particular task. + if(task.expiryTime > controller.getCycleEndTime()) { + IAutomationCore(automationCore).updateGasCommittedForNextCycle(task.taskType, task.maxGasAmount); + } + + emit TaskCancelled(_taskIndex, msg.sender, task.txHash); + } + + /// @notice Immediately stops automation tasks for the specified `_taskIndexes`. + /// Only tasks that exist and are owned by the sender can be stopped. + /// If any of the specified tasks are not owned by the sender, the transaction will abort. + /// When a task is stopped, the committed gas for the next cycle is reduced + /// by the max gas amount of the stopped task. Half of the remaining task fee is refunded. + /// @param _taskIndexes Array of task indexes to be stopped. + function stopTasks( + uint64[] memory _taskIndexes + ) external { + // Check if automation is enabled + IAutomationController controller = IAutomationController(automationController); + if (!controller.isAutomationEnabled()) { revert AutomationNotEnabled(); } + + if(!controller.isCycleStarted()) { revert CycleTransitionInProgress(); } + if(_taskIndexes.length == 0) { revert TaskIndexesCannotBeEmpty(); } + + LibRegistry.TaskStopped[] memory stoppedTaskDetails = new LibRegistry.TaskStopped[](_taskIndexes.length); + uint256 counter = 0; + + uint128 totalRefundFee = 0; + + // Calculate refundable fee for this remaining time task in current cycle + uint64 currentTime = uint64(block.timestamp); + uint64 cycleEndTime = controller.getCycleEndTime(); + uint64 residualInterval = cycleEndTime <= currentTime ? 0 : (cycleEndTime - currentTime); + + IAutomationCore core = IAutomationCore(automationCore); + + // Loop through each task index to validate and stop the task + for (uint256 i = 0; i < _taskIndexes.length; i++) { + if(ifTaskExists(_taskIndexes[i])) { + CommonUtils.TaskDetails memory task = regState.tasks[_taskIndexes[i]].getTaskDetails(); + + // Check if authorised + if(msg.sender != task.owner) { revert UnauthorizedAccount(); } + + // Check if UST + if(task.taskType == CommonUtils.TaskType.GST) { revert UnsupportedTaskOperation(); } + + // Remove task from the registry + _removeTask(_taskIndexes[i], false); + // Remove from active tasks + require(regState.activeTaskIds.remove(_taskIndexes[i]), TaskIndexNotFound()); + + // This check means the task was expected to be executed in the next cycle, but it has been stopped. + // We need to remove its gas commitment from `gasCommittedForNextCycle` for this particular task. + // Also it checks that task should not be cancelled. + if(task.state != CommonUtils.TaskState.CANCELLED && task.expiryTime > cycleEndTime) { + // Reduce committed gas by the stopped task's max gas + core.updateGasCommittedForNextCycle(task.taskType, task.maxGasAmount); + } + + (uint128 cycleFeeRefund, uint128 depositRefund) = core.unlockDepositAndCycleFee( + _taskIndexes[i], + task.state, + task.expiryTime, + task.maxGasAmount, + residualInterval, + uint64(currentTime), + task.depositFee + ); + totalRefundFee += (cycleFeeRefund + depositRefund); + + + // Add to stopped tasks + LibRegistry.TaskStopped memory taskStopped = LibRegistry.TaskStopped( + _taskIndexes[i], + depositRefund, + cycleFeeRefund, + task.txHash + ); + stoppedTaskDetails[counter] = taskStopped; + counter += 1; + } + } + + // Refund and emit event if any tasks were stopped + if(stoppedTaskDetails.length > 0) { + core.refund(msg.sender, totalRefundFee); + + // Emit task stopped event + emit TasksStopped( + stoppedTaskDetails, + msg.sender + ); + } + } + + /// @notice Immediately stops system automation tasks for the specified `_taskIndexes`. + /// Only tasks that exist and are owned by the sender can be stopped. + /// If any of the specified tasks are not owned by the sender, the transaction will abort. + /// When a task is stopped, the committed gas for the next cycle is reduced + /// by the max gas amount of the stopped task. + /// @param _taskIndexes Array of task indexes to be stopped. + function stopSystemTasks( + uint64[] memory _taskIndexes + ) external { + // Check if automation is enabled + IAutomationController controller = IAutomationController(automationController); + if (!controller.isAutomationEnabled()) { revert AutomationNotEnabled(); } + + if(!controller.isCycleStarted()) { revert CycleTransitionInProgress(); } + + // Ensure that task indexes are provided + if(_taskIndexes.length == 0) { revert TaskIndexesCannotBeEmpty(); } + + LibRegistry.TaskStopped[] memory stoppedTaskDetails = new LibRegistry.TaskStopped[](_taskIndexes.length); + uint256 counter = 0; + + // Loop through each task index to validate and stop the task + for (uint256 i = 0; i < _taskIndexes.length; i++) { + if(ifTaskExists(_taskIndexes[i])) { + CommonUtils.TaskDetails memory task = regState.tasks[_taskIndexes[i]].getTaskDetails(); + + if(task.owner != msg.sender) { revert UnauthorizedAccount(); } + + // Check if GST + if(task.taskType == CommonUtils.TaskType.UST) { revert UnsupportedTaskOperation(); } + _removeTask(_taskIndexes[i], true); + // Remove from active tasks + require(regState.activeTaskIds.remove(_taskIndexes[i]), TaskIndexNotFound()); + + if(task.state != CommonUtils.TaskState.CANCELLED && task.expiryTime > controller.getCycleEndTime()) { + IAutomationCore(automationCore).updateGasCommittedForNextCycle(task.taskType, task.maxGasAmount); + } + + // Add to stopped tasks + LibRegistry.TaskStopped memory taskStopped = LibRegistry.TaskStopped( + _taskIndexes[i], + 0, + 0, + task.txHash + ); + stoppedTaskDetails[counter] = taskStopped; + counter += 1; + } + } + + if(stoppedTaskDetails.length > 0) { + // Emit task stopped event + emit TasksStopped( + stoppedTaskDetails, + msg.sender + ); + } + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: HELPER FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Read tx hash via precompile. Reverts if precompile missing/fails. + function readTxHash() private view returns (bytes32) { + (bool ok, bytes memory out) = TX_HASH_PRECOMPILE.staticcall(""); + require(ok, FailedToCallTxHashPrecompile()); + require(out.length == 32, TxnHashLengthShouldBe32(uint64(out.length))); + return abi.decode(out, (bytes32)); + } + + /// @notice Function to remove a task from the registry. + /// @param _taskIndex Index of the task to remove. + /// @param _removeFromSysReg Wheather to remove from system task registry. + function _removeTask(uint64 _taskIndex, bool _removeFromSysReg) private { + if(_removeFromSysReg) { + require(regState.sysTaskIds.remove(_taskIndex), TaskIndexNotFound()); + } + + delete regState.tasks[_taskIndex]; + require(regState.taskIdList.remove(_taskIndex), TaskIndexNotFound()); + } + + /// @notice Function to ensure that AutomationController contract is the caller. + function onlyController() private view { + if(msg.sender != automationController) { revert CallerNotController(); } + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ADMIN FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Grants authorization to the input account to submit system automation tasks. + /// @param _account Address to grant authorization to. + function grantAuthorization(address _account) external onlyOwner { + require(regState.authorizedAccounts.add(_account), AddressAlreadyExists()); + emit AuthorizationGranted(_account, block.timestamp); + } + + /// @notice Revokes authorization from the input account to submit system automation tasks. + /// @param _account Address to revoke authorization from. + function revokeAuthorization(address _account) external onlyOwner { + require(regState.authorizedAccounts.remove(_account), AddressDoesNotExist()); + emit AuthorizationRevoked(_account, block.timestamp); + } + + /// @notice Function to update the AutomationCore contract address. + /// @param _automationCore Address of the AutomationCore contract. + function setAutomationCore(address _automationCore) external onlyOwner { + _automationCore.validateContractAddress(); + + address oldAutomationCore = automationCore; + automationCore = _automationCore; + + emit AutomationCoreUpdated(oldAutomationCore, _automationCore); + } + + /// @notice Function to update the AutomationController contract address. + /// @param _automationController Address of the AutomationController contract. + function setAutomationController(address _automationController) external onlyOwner { + _automationController.validateContractAddress(); + + address oldAutomationController = automationController; + automationController = _automationController; + + emit AutomationControllerUpdated(oldAutomationController, _automationController); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: CONTROLLER FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Internally calls _removeTask, reverts if caller is not AutomationController. + function removeTask(uint64 _taskIndex, bool _removeFromSysReg) external { + onlyController(); + _removeTask(_taskIndex, _removeFromSysReg); + } + + /// @notice Function to update state of the task. + /// @param _taskIndex Index of the task. + /// @param _taskState State to update task to. + function updateTaskState(uint64 _taskIndex, CommonUtils.TaskState _taskState) external { + onlyController(); + LibRegistry.setState(regState.tasks[_taskIndex], uint8(_taskState)); + } + + /// @notice Function to update tasks lists. + /// @param _state Cycle transition state executing the update. + function updateTaskIds(CommonUtils.CycleState _state) external { + onlyController(); + + regState.activeTaskIds.clear(); + + if(_state == CommonUtils.CycleState.FINISHED) { + uint256[] memory taskIds = regState.taskIdList.values(); + for (uint256 i = 0; i < taskIds.length; i++) { + regState.activeTaskIds.add(taskIds[i]); + } + } else { + regState.sysTaskIds.clear(); + } + } + + /// @notice Refunds the deposit fee of the task and removes from the registry. + /// @param _taskIndex Index of the task. + /// @param _taskOwner Owner of the task. + /// @param _refundableDeposit Refundable amount of deposit. + /// @param _lockedDeposit Total locked deposit. + function refundDepositAndDrop( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) external { + onlyController(); + // Check if task is UST + if (regState.tasks[_taskIndex].taskType() == CommonUtils.TaskType.GST) { revert RegisteredTaskInvalidType(); } + + // Remove task from the registry state + _removeTask(_taskIndex, false); + + // Refund + IAutomationCore(automationCore).safeDepositRefund( + _taskIndex, + _taskOwner, + _refundableDeposit, + _lockedDeposit + ); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: VIEW FUNCTIONS :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Retrieves the details of automation tasks by their task index. Skips a task if it doesn't exist. + /// @param _taskIndexes Input task indexes to get details of. + /// @return Task details of the tasks that exist. + function getTaskDetailsBulk(uint64[] memory _taskIndexes) external view returns (CommonUtils.TaskDetails[] memory) { + uint256 count = _taskIndexes.length; + CommonUtils.TaskDetails[] memory temp = new CommonUtils.TaskDetails[](count); + uint256 exists; + + for (uint256 i = 0; i < count; i++) { + if(ifTaskExists(_taskIndexes[i])) { + temp[exists] = regState.tasks[_taskIndexes[i]].getTaskDetails(); + exists += 1; + } + } + + CommonUtils.TaskDetails[] memory taskDetails = new CommonUtils.TaskDetails[](exists); + for (uint256 i = 0; i < exists; i++) { + taskDetails[i] = temp[i]; + } + return taskDetails; + } + + /// @notice Returns all the automation tasks available in the registry. + function getTaskIdList() external view returns (uint256[] memory) { + return regState.taskIdList.values(); + } + + /// @notice Returns the number of total tasks. + function totalTasks() public view returns (uint256) { + return regState.taskIdList.length(); + } + + /// @notice Returns the number of total system tasks. + function totalSystemTasks() public view returns (uint256) { + return regState.sysTaskIds.length(); + } + + /// @notice Returns the next task index. + function getNextTaskIndex() external view returns (uint64) { + return regState.currentIndex; + } + + /// @notice Returns the details of a task. Reverts if task doesn't exist. + /// @param _taskIndex Task index to get details for. + function getTaskDetails(uint64 _taskIndex) external view returns (CommonUtils.TaskDetails memory) { + if(!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + return regState.tasks[_taskIndex].getTaskDetails(); + } + + /// @notice Checks if a task exist. + /// @param _taskIndex Task index to check if a task exists against it. + function ifTaskExists(uint64 _taskIndex) public view returns (bool) { + return regState.tasks[_taskIndex].owner() != address(0) && regState.taskIdList.contains(_taskIndex); + } + + /// @notice Checks if a system task exist. + /// @param _taskIndex Task index to check if a system task exists against it. + function ifSysTaskExists(uint64 _taskIndex) public view returns (bool) { + return regState.sysTaskIds.contains(_taskIndex); + } + + /// @notice Validates the input task type against the task type. + /// @param _taskIndex Index of the task. + /// @param _type Input task type. + function checkTaskType(uint64 _taskIndex, CommonUtils.TaskType _type) external view returns (bool) { + if (!ifTaskExists(_taskIndex)) { revert TaskDoesNotExist(); } + return _type == regState.tasks[_taskIndex].taskType(); + } + + /// @notice Returns the owner of the task + /// @param _taskIndex Task index of the task to query. + function getTaskOwner(uint64 _taskIndex) external view returns (address) { + return regState.tasks[_taskIndex].owner(); + } + + /// @notice Returns the state of the task + /// @param _taskIndex Task index of the task to query. + function getTaskState(uint64 _taskIndex) external view returns (CommonUtils.TaskState) { + return LibRegistry.state(regState.tasks[_taskIndex]); + } + + /// @notice Checks if the input account is an authorized submitter to submit system automation tasks. + /// @param _account Address to check if it's authorized. + function isAuthorizedSubmitter(address _account) public view returns (bool) { + return regState.authorizedAccounts.contains(_account); + } + + /// @notice Returns the total number of active tasks. + function getTotalActiveTasks() external view returns (uint256) { + return regState.activeTaskIds.length(); + } + + /// @notice Returns all the active task indexes. + function getAllActiveTaskIds() external view returns (uint256[] memory) { + return regState.activeTaskIds.values(); + } + + /// @notice Checks whether there is an active task in registry with specified input task index. + function hasActiveUserTask(address _account, uint64 _taskIndex) external view returns (bool) { + return hasActiveTaskOfType(_account, _taskIndex, CommonUtils.TaskType.UST); + } + + /// @notice Checks whether there is an active system task in registry with specified input task index. + function hasActiveSystemTask(address _account, uint64 _taskIndex) external view returns (bool) { + return hasActiveTaskOfType(_account, _taskIndex, CommonUtils.TaskType.GST); + } + + /// @notice Checks whether there is an active task in registry with specified input task index of the input type. + /// The type can be either 0 for user submitted tasks, and 1 for governance authorized tasks. + function hasActiveTaskOfType(address _account, uint64 _taskIndex, CommonUtils.TaskType _type) public view returns (bool) { + LibRegistry.TaskMetadata storage task = regState.tasks[_taskIndex]; + return task.owner() == _account && task.state() != CommonUtils.TaskState.PENDING && task.taskType() == _type; + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: UPGRADEABILITY FUNCTIONS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Helper function that reverts when 'msg.sender' is not authorized to upgrade the contract. + /// @dev called by 'upgradeTo' and 'upgradeToAndCall' in UUPSUpgradeable + /// @dev must be called by 'owner' + /// @param newImplementation address of the new implementation + function _authorizeUpgrade(address newImplementation) internal virtual override onlyOwner{ } +} diff --git a/solidity/supra_contracts/src/BlockMeta.sol b/solidity/supra_contracts/src/BlockMeta.sol index 6c35446501..5f839debb3 100644 --- a/solidity/supra_contracts/src/BlockMeta.sol +++ b/solidity/supra_contracts/src/BlockMeta.sol @@ -21,6 +21,7 @@ contract BlockMeta is OwnableUpgradeable, UUPSUpgradeable { * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */ + /// @notice Ordered list of functions to be executed /// @dev Layout: [target[160] | selector[32] | 0[64]] uint256[] private executions; @@ -89,6 +90,7 @@ contract BlockMeta is OwnableUpgradeable, UUPSUpgradeable { /// @param _selector Function selector to be called on target contract. function register(address _targetContract, bytes4 _selector) external onlyOwner { _targetContract.validateContractAddress(); + require(_selector != bytes4(0), InvalidSelector()); uint256 executionEntry = packExecution(_targetContract, _selector); diff --git a/solidity/supra_contracts/src/CommonUtils.sol b/solidity/supra_contracts/src/CommonUtils.sol index 3f0f6e29ef..14215de15a 100644 --- a/solidity/supra_contracts/src/CommonUtils.sol +++ b/solidity/supra_contracts/src/CommonUtils.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.27; +import {LibRegistry} from "./LibRegistry.sol"; // Helper library used by supra contracts library CommonUtils { @@ -12,6 +13,91 @@ library CommonUtils { // Address of the VM Signer: SUP0 address constant VM_SIGNER = address(0x53555000); + /// @notice Enum describing state of the cycle. + enum CycleState { + READY, + STARTED, + FINISHED, + SUSPENDED + } + + /// @notice Enum describing state of a task. + enum TaskState { + PENDING, + ACTIVE, + CANCELLED + } + + /// @notice Enum describing task type. + enum TaskType { + UST, + GST + } + + /// @notice Task details for individual automation tasks. + struct TaskDetails { + uint128 maxGasAmount; + uint128 gasPriceCap; + uint128 automationFeeCapForCycle; + uint128 depositFee; + bytes32 txHash; + uint64 taskIndex; + uint64 registrationTime; + uint64 expiryTime; + uint64 priority; + TaskType taskType; + TaskState state; + address owner; + bytes payloadTx; + bytes[] auxData; + } + + function getTaskDetails(LibRegistry.TaskMetadata storage t) internal view returns (TaskDetails memory details) { + // --- Decode maxGasAmount (upper 128 bits) --- + details.maxGasAmount = uint128(t.maxGasAmount_gasPriceCap >> 128); + + // --- Decode gasPriceCap (lower 128 bits) --- + details.gasPriceCap = uint128(t.maxGasAmount_gasPriceCap); + + // --- Decode automationFeeCapForCycle (upper 128 bits) --- + details.automationFeeCapForCycle = uint128(t.automationFeeCapForCycle_depositFee >> 128); + + // --- Decode depositFee (lower 128 bits) --- + details.depositFee = uint128(t.automationFeeCapForCycle_depositFee); + + // --- Direct values --- + details.txHash = t.txHash; + details.payloadTx = t.payloadTx; + details.auxData = t.auxData; + + // --- Decode packed uint256: taskIndex | registrationTime | expiryTime | priority --- + details.taskIndex = uint64(t.taskIndex_registrationTime_expiryTime_priority >> 192); + details.registrationTime = uint64(t.taskIndex_registrationTime_expiryTime_priority >> 128); + details.expiryTime = uint64(t.taskIndex_registrationTime_expiryTime_priority >> 64); + details.priority = uint64(t.taskIndex_registrationTime_expiryTime_priority); + + // --- Decode packed uint256: owner | taskType | taskState --- + details.owner = address(uint160(t.owner_type_state >> 96)); + details.taskType = TaskType(uint8(t.owner_type_state >> 88)); + details.state = TaskState(uint8(t.owner_type_state >> 80)); + } + + + /// @notice Deposit and fee related accounting. + struct Deposit { + uint256 totalDepositedAutomationFees; + address coldWallet; + // mapping(uint64 => uint256) taskLockedFees; // TO_DO + } + + /// @notice Struct representing a stopped task. + struct TaskStopped { + uint64 taskIndex; + uint128 depositRefund; + uint128 cycleFeeRefund; + bytes32 txHash; + } + /// @dev Returns a boolean indicating whether the given address is a contract or not. /// @param _addr The address to be checked. /// @return A boolean indicating whether the given address is a contract or not. diff --git a/solidity/supra_contracts/src/IAutomationController.sol b/solidity/supra_contracts/src/IAutomationController.sol new file mode 100644 index 0000000000..465789d8bb --- /dev/null +++ b/solidity/supra_contracts/src/IAutomationController.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {CommonUtils} from "./CommonUtils.sol"; + +interface IAutomationController { + // Custom errors + error AlreadyEnabled(); + error AlreadyDisabled(); + error CallerNotVmSigner(); + error InconsistentTransitionState(); + error InvalidInputCycleIndex(); + error InvalidRegistryState(); + error OutOfOrderTaskProcessingRequest(); + error RefundFailed(); + error RefundDepositAndDropFailed(); + error RemoveTaskFailed(); + error TransferFailed(); + error UnlockLockedDepositFailed(); + error UpdateGasCommittedAndCycleLockedFeesFailed(); + error UpdateTaskStateFailed(); + + // View functions + function getCycleInfo() external view returns (uint64, uint64, uint64, CommonUtils.CycleState); + function getCycleDuration() external view returns (uint64); + function getCycleEndTime() external view returns (uint64 cycleEndTime); + function getTransitionInfo() external view returns (uint64, uint128); + function isAutomationEnabled() external view returns (bool); + function isCycleStarted() external view returns (bool); + function isTransitionInProgress() external view returns (bool); + + // State updating functions + function monitorCycleEnd() external; +} diff --git a/solidity/supra_contracts/src/IAutomationCore.sol b/solidity/supra_contracts/src/IAutomationCore.sol new file mode 100644 index 0000000000..1fc8a80b6f --- /dev/null +++ b/solidity/supra_contracts/src/IAutomationCore.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {CommonUtils} from "./CommonUtils.sol"; + +interface IAutomationCore { + // Custom errors + error AddressCannotBeZero(); + error AutomationNotEnabled(); + error CallerNotController(); + error CallerNotRegistry(); + error CycleTransitionInProgress(); + error ErrorDepositRefund(); + error ErrorCycleFeeRefund(); + error InvalidAmount(); + error InvalidMaxGasAmount(); + error InvalidTaskType(); + error InvalidTxHash(); + error AlreadyEnabled(); + error AlreadyDisabled(); + error GasCommittedExceedsMaxGasCap(); + error GasCommittedValueUnderflow(); + error InsufficientBalance(); + error InsufficientFeeCapForCycle(); + error InsufficientBalanceForRefund(); + error InvalidCongestionExponent(); + error InvalidCongestionThreshold(); + error InvalidCycleDuration(); + error InvalidExpiryTime(); + error InvalidGasPriceCap(); + error InvalidRegistryMaxGasCap(); + error InvalidSysRegistryMaxGasCap(); + error InvalidSysTaskCapacity(); + error InvalidSysTaskDuration(); + error InvalidTaskCapacity(); + error InvalidTaskDuration(); + error RegistrationDisabled(); + error RequestExceedsLockedBalance(); + error TaskCapacityReached(); + error TaskExpiresBeforeNextCycle(); + error TransferFailed(); + error UnacceptableRegistryMaxGasCap(); + error UnacceptableSysRegistryMaxGasCap(); + error UnauthorizedCaller(); + + // View functions + function flatRegistrationFeeWei() external view returns (uint128); + function getAutomationController() external view returns (address); + function erc20Supra() external view returns (address); + function calculateTaskFee( + CommonUtils.TaskState _state, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _potentialFeeTimeframe, + uint64 _currentTime, + uint128 _automationFeePerSec + ) external view returns (uint128); + function calculateAutomationFeeMultiplierForCurrentCycleInternal() external view returns (uint128); + function calculateAutomationFeeMultiplierForCommittedOccupancy(uint128 _totalCommittedMaxGas) external view returns (uint128); + function cycleDurationSecs() external view returns (uint64); + function getVmSigner() external view returns (address); + function getGasCommittedForNextCycle() external view returns (uint128); + function getCycleLockedFees() external view returns (uint256); + function getTotalDepositedAutomationFees() external view returns (uint256); + function updateStateForValidRegistration( + uint256 _totalTasks, + uint64 _regTime, + uint64 _expiryTime, + CommonUtils.TaskType _taskType, + bytes memory _payloadTx, + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle + ) external; + + // State updating functions + function applyPendingConfig() external returns (bool, uint64); + function incTotalDepositedAutomationFees(uint256 _totalDepositedAutomationFees) external; + function chargeFees(address _from, uint256 _amount) external; + function safeUnlockLockedDeposit( + uint64 _taskIndex, + uint128 _lockedDeposit + ) external returns (bool); + function refundTaskFees( + uint64 _currentTime, + uint64 _refundDuration, + uint128 _automationFeePerSec, + CommonUtils.TaskDetails memory _task + ) external; + function safeDepositRefund( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) external returns (bool); + function refund(address _to, uint128 _amount) external; + function unlockDepositAndCycleFee( + uint64 _taskIndex, + CommonUtils.TaskState _taskState, + uint64 _expiryTime, + uint128 _maxGasAmount, + uint64 _residualInterval, + uint64 _currentTime, + uint128 _depositFee + ) external returns (uint128, uint128); + function updateGasCommittedForNextCycle(CommonUtils.TaskType _taskType, uint128 _maxGasAmount) external; + function updateGasCommittedAndCycleLockedFees( + uint256 _lockedFees, + uint128 _sysGasCommittedForNextCycle, + uint128 _gasCommittedForNextCycle, + uint128 _gasCommittedForNewCycle + ) external; +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/IAutomationRegistry.sol b/solidity/supra_contracts/src/IAutomationRegistry.sol new file mode 100644 index 0000000000..8c0462a0cb --- /dev/null +++ b/solidity/supra_contracts/src/IAutomationRegistry.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {CommonUtils} from "./CommonUtils.sol"; + +interface IAutomationRegistry { + // Custom errors + error AddressAlreadyExists(); + error AddressDoesNotExist(); + error AutomationNotEnabled(); + error CallerNotController(); + error UnauthorizedAccount(); + error CycleTransitionInProgress(); + error TaskDoesNotExist(); + error UnsupportedTaskOperation(); + error AlreadyCancelled(); + error ErrorDepositRefund(); + error SystemTaskDoesNotExist(); + error TaskIndexesCannotBeEmpty(); + error RegisteredTaskInvalidType(); + error TaskIndexNotFound(); + error TaskIndexNotUnique(); + error FailedToCallTxHashPrecompile(); + error TxnHashLengthShouldBe32(uint64); + + // View functions + function ifTaskExists(uint64 _taskIndex) external view returns (bool); + function checkTaskType(uint64 _taskIndex, CommonUtils.TaskType _type) external view returns (bool); + function getAllActiveTaskIds() external view returns (uint256[] memory); + function getTaskDetails(uint64 _taskIndex) external view returns (CommonUtils.TaskDetails memory); + function getTaskIdList() external view returns (uint256[] memory); + function getTotalActiveTasks() external view returns (uint256); + function totalTasks() external view returns (uint256); + + // State updating functions + function removeTask(uint64 _taskIndex, bool _removeFromSysReg) external; + function updateTaskState(uint64 _taskIndex, CommonUtils.TaskState _taskState) external; + function updateTaskIds(CommonUtils.CycleState _state) external; + function refundDepositAndDrop( + uint64 _taskIndex, + address _taskOwner, + uint128 _refundableDeposit, + uint128 _lockedDeposit + ) external; +} diff --git a/solidity/supra_contracts/src/LibConfig.sol b/solidity/supra_contracts/src/LibConfig.sol new file mode 100644 index 0000000000..ec86fa22c1 --- /dev/null +++ b/solidity/supra_contracts/src/LibConfig.sol @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +// Helper library used by AutomationConfig. +library LibConfig { + uint256 private constant MAX_UINT128 = type(uint128).max; + uint256 private constant MAX_UINT160 = type(uint160).max; + uint256 private constant MAX_UINT64 = type(uint64).max; + uint256 private constant MAX_UINT16 = type(uint16).max; + uint256 private constant MAX_UINT8 = type(uint8).max; + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: AccessListEntry ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Struct representing an entry in access list. + struct AccessListEntry { + address addr; + bytes32[] storageKeys; + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ConfigBuffer ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Struct representing configuration buffer. + struct ConfigBuffer { + Config pendingConfig; + bool ifExists; + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: RegistryConfig ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Configuration of the automation registry. + struct RegistryConfig { + // uint128 | uint128 + uint256 gasCommittedForNextCycle_gasCommittedForThisCycle; + // uint128 | uint128 + uint256 sysGasCommittedForNextCycle_sysGasCommittedForThisCycle; + + // uint128 | uint128 + uint256 nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap; + // address | bool(1 bit) + uint256 controller_registrationEnabled; + uint256 cycleLockedFees; + uint256 totalDepositedAutomationFees; + address vmSigner; + address erc20Supra; + address registry; + Config config; + } + + function createRegistryConfig( + uint128 _nextCycleRegistryMaxGasCap, + uint128 _nextCycleSysRegistryMaxGasCap, + bool _registrationEnabled, + address _vmSigner, + address _erc20Supra, + Config memory _config + ) internal pure returns (RegistryConfig memory rcfg) { + // Pack nextCycleRegistryMaxGasCap | nextCycleSysRegistryMaxGasCap + rcfg.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap = + (uint256(_nextCycleRegistryMaxGasCap) << 128) | + uint256(_nextCycleSysRegistryMaxGasCap); + + // Pack controller (address) | registrationEnabled (bool at bit 95) + // Sets controller as address(0) + rcfg.controller_registrationEnabled = _registrationEnabled ? uint256(1) << 95 : 0; + + rcfg.vmSigner = _vmSigner; + rcfg.erc20Supra = _erc20Supra; + + // Assign inner Config + rcfg.config = _config; + } + + // gasCommittedForNextCycle (uint128) | gasCommittedForThisCycle (uint128) + function gasCommittedForNextCycle(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.gasCommittedForNextCycle_gasCommittedForThisCycle >> 128); + } + + function gasCommittedForThisCycle(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.gasCommittedForNextCycle_gasCommittedForThisCycle); + } + + function setGasCommittedForNextCycle(RegistryConfig storage r, uint128 _value) internal { + // Clear upper 128 bits + r.gasCommittedForNextCycle_gasCommittedForThisCycle &= MAX_UINT128; + // Insert new upper 128 bits + r.gasCommittedForNextCycle_gasCommittedForThisCycle |= uint256(_value) << 128; + } + + function setGasCommittedForThisCycle(RegistryConfig storage r, uint128 _value) internal { + // Clear lower 128 bits + r.gasCommittedForNextCycle_gasCommittedForThisCycle &= MAX_UINT128 << 128; + // Insert new lower 128 bits + r.gasCommittedForNextCycle_gasCommittedForThisCycle |= uint256(_value); + } + + // sysGasCommittedForNextCycle (uint128) | sysGasCommittedForThisCycle (uint128) + function sysGasCommittedForNextCycle(RegistryConfig storage r) internal view returns (uint128){ + return uint128(r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle >> 128); + } + + function sysGasCommittedForThisCycle(RegistryConfig storage r) internal view returns (uint128){ + return uint128(r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle); + } + + function setSysGasCommittedForNextCycle(RegistryConfig storage r, uint128 _value) internal { + // Clear upper 128 bits + r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle &= MAX_UINT128; // mask = lower 128 bits all 1s + + // Insert new upper 128 bits + r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle |= uint256(_value) << 128; + } + + function setSysGasCommittedForThisCycle(RegistryConfig storage r, uint128 _value) internal { + // Clear lower 128 bits + r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle &= MAX_UINT128 << 128; // mask = upper 128 bits all 1s + + // Insert new lower 128 bits + r.sysGasCommittedForNextCycle_sysGasCommittedForThisCycle |= uint256(_value); + } + + // nextCycleRegistryMaxGasCap (uint128) | nextCycleSysRegistryMaxGasCap (uint128) + function nextCycleRegistryMaxGasCap(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap >> 128); + } + + function nextCycleSysRegistryMaxGasCap(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap); + } + + function setNextCycleRegistryMaxGasCap(RegistryConfig storage r, uint128 value) internal { + // clear upper 128 bits then set + r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap &= MAX_UINT128; + r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap |= uint256(value) << 128; + } + + function setNextCycleSysRegistryMaxGasCap(RegistryConfig storage r, uint128 value) internal { + // clear lower 128 bits then set + r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap &= (MAX_UINT128 << 128); + r.nextCycleRegistryMaxGasCap_nextCycleSysRegistryMaxGasCap |= uint256(value); + } + + // controller (address) | registrationEnabled (bool)[bit 95] + function automationController(RegistryConfig storage r) internal view returns (address) { + return address(uint160(r.controller_registrationEnabled >> 96)); + } + + function registrationEnabled(RegistryConfig storage r) internal view returns (bool) { + return (r.controller_registrationEnabled >> 95) & 1 != 0; + } + + function setAutomationController(RegistryConfig storage r, address _controller) internal { + // clear top 160 bits + r.controller_registrationEnabled &= ~(MAX_UINT160 << 96); + + // insert 160-bit address + r.controller_registrationEnabled |= uint256(uint160(_controller)) << 96; + } + + function setRegistrationEnabled(RegistryConfig storage r, bool enabled) internal { + // clear bit 95 + r.controller_registrationEnabled &= ~(uint256(1) << 95); + + // set bit 95 if enabled + r.controller_registrationEnabled |= enabled ? (uint256(1) << 95) : 0; + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Config ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Struct representing configuration parameters. + struct Config { + // uint128 | uint128 + uint256 registryMaxGasCap_sysRegistryMaxGasCap; + // uint128 | uint128 // TO_DO: need to decide on the currency + uint256 automationBaseFeeWeiPerSec_flatRegistrationFeeWei; + // uint128 | uint64 | uint64 // TO_DO: need to decide on the currency + uint256 congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs; + // uint64 | uint16 | uint16 | uint8 | uint8 + uint256 cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent; + } + + function createConfig( + uint128 _registryMaxGasCap, + uint128 _sysRegistryMaxGasCap, + uint128 _automationBaseFeeWeiPerSec, + uint128 _flatRegistrationFeeWei, + uint128 _congestionBaseFeeWeiPerSec, + uint64 _taskDurationCapSecs, + uint64 _sysTaskDurationCapSecs, + uint64 _cycleDurationSecs, + uint16 _taskCapacity, + uint16 _sysTaskCapacity, + uint8 _congestionThresholdPercentage, + uint8 _congestionExponent + ) internal pure returns (Config memory cfg) { + // Pack registryMaxGasCap | sysRegistryMaxGasCap + cfg.registryMaxGasCap_sysRegistryMaxGasCap = (uint256(_registryMaxGasCap) << 128) | uint256(_sysRegistryMaxGasCap); + + // Pack automationBaseFeeWeiPerSec | flatRegistrationFeeWei + cfg.automationBaseFeeWeiPerSec_flatRegistrationFeeWei = (uint256(_automationBaseFeeWeiPerSec) << 128) | uint256(_flatRegistrationFeeWei); + + // Pack congestionBaseFeeWeiPerSec | taskDurationCapSecs | sysTaskDurationCapSecs + cfg.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs = + (uint256(_congestionBaseFeeWeiPerSec) << 128) | + (uint256(_taskDurationCapSecs) << 64) | + uint256(_sysTaskDurationCapSecs); + + // Pack cycleDurationSecs | taskCapacity | sysTaskCapacity | congestionThresholdPercentage | congestionExponent + cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent = + (uint256(_cycleDurationSecs) << 192) | + (uint256(_taskCapacity) << 176) | + (uint256(_sysTaskCapacity) << 160) | + (uint256(_congestionThresholdPercentage) << 152) | + (uint256(_congestionExponent) << 144); + } + + // uint256 registryMaxGasCap (uint128) | sysRegistryMaxGasCap (uint128) + function registryMaxGasCap(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.config.registryMaxGasCap_sysRegistryMaxGasCap >> 128); + } + + function sysRegistryMaxGasCap(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.config.registryMaxGasCap_sysRegistryMaxGasCap); + } + + function setRegistryMaxGasCap(RegistryConfig storage r, uint128 value) internal { + r.config.registryMaxGasCap_sysRegistryMaxGasCap &= MAX_UINT128; + r.config.registryMaxGasCap_sysRegistryMaxGasCap |= uint256(value) << 128; + } + + function setSysRegistryMaxGasCap(RegistryConfig storage r, uint128 value) internal { + r.config.registryMaxGasCap_sysRegistryMaxGasCap &= (MAX_UINT128 << 128); + r.config.registryMaxGasCap_sysRegistryMaxGasCap |= uint256(value); + } + + // automationBaseFeeWeiPerSec (uint128) | flatRegistrationFeeWei (uint128) + function automationBaseFeeWeiPerSec(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei >> 128); + } + + function flatRegistrationFeeWei(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei); + } + + function setAutomationBaseFeeWeiPerSec(RegistryConfig storage r, uint128 value) internal { + r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei &= MAX_UINT128; + r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei |= uint256(value) << 128; + } + + function setFlatRegistrationFeeWei(RegistryConfig storage r, uint128 value) internal { + r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei &= (MAX_UINT128 << 128); + r.config.automationBaseFeeWeiPerSec_flatRegistrationFeeWei |= uint256(value); + } + + // congestionBaseFeeWeiPerSec (uint128) | taskDurationCapSecs (uint64) | sysTaskDurationCapSecs (uint64) + function congestionBaseFeeWeiPerSec(RegistryConfig storage r) internal view returns (uint128) { + return uint128(r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs >> 128); + } + + function taskDurationCapSecs(RegistryConfig storage r) internal view returns (uint64) { + return uint64(r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs >> 64); + } + + function sysTaskDurationCapSecs(RegistryConfig storage r) internal view returns (uint64) { + return uint64(r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs); + } + + function setCongestionBaseFeeWeiPerSec(RegistryConfig storage r, uint128 _value) internal { + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs &= MAX_UINT128; + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs |= uint256(_value) << 128; + } + + function setTaskDurationCapSecs(RegistryConfig storage r, uint64 value) internal { + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs &= ~(MAX_UINT64 << 64); + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs |= uint256(value) << 64; + } + + function setSysTaskDurationCapSecs(RegistryConfig storage r, uint64 value) internal { + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs &= ~MAX_UINT64; + r.config.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs |= uint256(value); + } + + // cycleDurationSecs (uint64) | taskCapacity (uint16) | sysTaskCapacity (uint16) | congestionThresholdPercentage (uint8) | congestionExponent (uint8) + function cycleDurationSecs(Config storage c) internal view returns (uint64) { + return uint64(c.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 192); + } + + function taskCapacity(RegistryConfig storage r) internal view returns (uint16) { + return uint16(r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 176); + } + + function sysTaskCapacity(RegistryConfig storage r) internal view returns (uint16) { + return uint16(r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 160); + } + + function congestionThresholdPercentage(RegistryConfig storage r) internal view returns (uint8) { + return uint8(r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 152); + } + + function congestionExponent(RegistryConfig storage r) internal view returns (uint8) { + return uint8(r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 144); + } + + function setCycleDurationSecs(RegistryConfig storage r, uint64 _value) internal { + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent &= ~(MAX_UINT64 << 192); + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent |= uint256(_value) << 192; + } + + function setTaskCapacity(RegistryConfig storage r, uint16 _value) internal { + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent &= ~(MAX_UINT16 << 176); + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent |= uint256(_value) << 176; + } + + function setSysTaskCapacity(RegistryConfig storage r, uint16 _value) internal { + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent &= ~(MAX_UINT16 << 160); + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent |= uint256(_value) << 160; + } + + function setCongestionThresholdPercentage(RegistryConfig storage r, uint8 _value) internal { + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent &= ~(MAX_UINT8 << 152); + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent |= uint256(_value) << 152; + } + + function setCongestionExponent(RegistryConfig storage r, uint8 _value) internal { + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent &= ~(MAX_UINT8 << 144); + r.config.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent |= uint256(_value) << 144; + } + + /// @notice Struct representing configuration details. + struct ConfigDetails { + uint128 registryMaxGasCap; + uint128 sysRegistryMaxGasCap; + uint128 automationBaseFeeWeiPerSec; // TO_DO: need to decide on the currency + uint128 flatRegistrationFeeWei; // TO_DO: need to decide on the currency + uint128 congestionBaseFeeWeiPerSec; // TO_DO: need to decide on the currency + uint64 taskDurationCapSecs; + uint64 sysTaskDurationCapSecs; + uint64 cycleDurationSecs; + uint16 taskCapacity; + uint16 sysTaskCapacity; + uint8 congestionThresholdPercentage; + uint8 congestionExponent; + } + + function getConfig(Config memory cfg) internal pure returns (ConfigDetails memory config) { + // ------------------------------------------------------------- + // 1. registryMaxGasCap (high 128) | sysRegistryMaxGasCap (low 128) + // ------------------------------------------------------------- + config.registryMaxGasCap = uint128(cfg.registryMaxGasCap_sysRegistryMaxGasCap >> 128); + config.sysRegistryMaxGasCap = uint128(cfg.registryMaxGasCap_sysRegistryMaxGasCap); + + // ------------------------------------------------------------- + // 2. automationBaseFeeWeiPerSec (high 128) | flatRegistrationFeeWei (low 128) + // ------------------------------------------------------------- + config.automationBaseFeeWeiPerSec = uint128(cfg.automationBaseFeeWeiPerSec_flatRegistrationFeeWei >> 128); + config.flatRegistrationFeeWei = uint128(cfg.automationBaseFeeWeiPerSec_flatRegistrationFeeWei); + + // ------------------------------------------------------------- + // 3. congestionBaseFeeWeiPerSec (high 128) + // taskDurationCapSecs (next 64) + // sysTaskDurationCapSecs (low 64) + // ------------------------------------------------------------- + config.congestionBaseFeeWeiPerSec = uint128(cfg.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs >> 128); + config.taskDurationCapSecs = uint64(cfg.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs >> 64); + config.sysTaskDurationCapSecs = uint64(cfg.congestionBaseFeeWeiPerSec_taskDurationCapSecs_sysTaskDurationCapSecs); + + // ------------------------------------------------------------- + // 4. cycleDurationSecs (high 64) + // taskCapacity (next 16) + // sysTaskCapacity (next 16) + // congestionThresholdPercentage (next 8) + // congestionExponent (low 8) + // ------------------------------------------------------------- + config.cycleDurationSecs = uint64(cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 192); + config.taskCapacity = uint16(cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 176); + config.sysTaskCapacity = uint16(cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 160); + config.congestionThresholdPercentage = uint8(cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 152); + config.congestionExponent = uint8(cfg.cycleDurationSecs_taskCapacity_sysTaskCapacity_congestionThresholdPercentage_congestionExponent >> 144); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/src/LibController.sol b/solidity/supra_contracts/src/LibController.sol new file mode 100644 index 0000000000..f6f07b7a7e --- /dev/null +++ b/solidity/supra_contracts/src/LibController.sol @@ -0,0 +1,234 @@ + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {EnumerableSet} from "../lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {CommonUtils} from "./CommonUtils.sol"; + +// Helper library used by AutomationController. +library LibController { + + uint256 private constant MAX_UINT128 = type(uint128).max; + uint256 private constant MAX_UINT64 = type(uint64).max; + uint256 private constant MAX_UINT8 = type(uint8).max; + + /// @notice Struct representing the state of current cycle. + struct AutomationCycleInfo{ + // uint64 | uint64 | uint64 | CycleState(uint8) | bool(1 bit) | bool(1 bit) + uint256 index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled; + TransitionState transitionState; + } + + /// @notice Struct representing state transition information. + struct TransitionState { + uint256 lockedFees; + // uint128 | uint128; + uint256 automationFeePerSec_gasCommittedForNewCycle; + // uint128 | uint128 + uint256 gasCommittedForNextCycle_sysGasCommittedForNextCycle; + // uint64 | uint64 | uint64 + uint256 refundDuration_newCycleDuration_nextTaskIndexPosition; + EnumerableSet.UintSet expectedTasksToBeProcessed; + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: AutomationCycleInfo :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + function initializeCycle( + AutomationCycleInfo storage _cycleInfo, + uint64 _index, + uint64 _startTime, + uint64 _durationSecs, + CommonUtils.CycleState _cycleState, + bool _automationEnabled + ) internal { + _cycleInfo.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled = + (uint256(_index) << 192) | + (uint256(_startTime) << 128) | + (uint256(_durationSecs) << 64) | + (uint256(_cycleState) << 56) | + (_automationEnabled ? (uint256(1) << 54) : 0); + } + + // index(uint64) | startTime(uint64) | durationSecs(uint64) | state(CycleState/uint8) | ifTransitionStateExists(bool)[bit 55] | automationEnabled(bool)[bit 54] + function index(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 192); + } + + function startTime(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 128); + } + + function durationSecs(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 64); + } + + function state(AutomationCycleInfo storage cycle) internal view returns (CommonUtils.CycleState) { + return CommonUtils.CycleState(uint8(cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 56)); + } + + function ifTransitionStateExists(AutomationCycleInfo storage cycle) internal view returns (bool) { + return ((cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 55) & 1) != 0; + } + + function automationEnabled(AutomationCycleInfo storage cycle) internal view returns (bool) { + return ((cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled >> 54) & 1) != 0; + } + + function setIndex(AutomationCycleInfo storage cycle, uint64 _index) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(MAX_UINT64 << 192); // Clear old bits + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= uint256(_index) << 192; // Set new value + } + + function setStartTime(AutomationCycleInfo storage cycle, uint64 _startTime) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(MAX_UINT64 << 128); + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= uint256(_startTime) << 128; + } + + function setDurationSecs(AutomationCycleInfo storage cycle, uint64 _durationSecs) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(MAX_UINT64 << 64); + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= uint256(_durationSecs) << 64; + } + + function setState(AutomationCycleInfo storage cycle, uint8 _state) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(MAX_UINT8 << 56); + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= uint256(_state) << 56; + } + + function setTransitionStateExists(AutomationCycleInfo storage cycle, bool exists) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(uint256(1) << 55); + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= exists ? (uint256(1) << 55) : 0; + } + + function setAutomationEnabled(AutomationCycleInfo storage cycle, bool enabled) internal { + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled &= ~(uint256(1) << 54); + cycle.index_startTime_durationSecs_state_ifTransitionStateExists_automationEnabled |= enabled ? (uint256(1) << 54) : 0; + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: TransitionState :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + // automationFeePerSec (uint128) | gasCommittedForNewCycle (uint128) + function automationFeePerSec(AutomationCycleInfo storage cycle) internal view returns (uint128) { + return uint128(cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle >> 128); + } + + function gasCommittedForNewCycle(AutomationCycleInfo storage cycle) internal view returns (uint128) { + return uint128(cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle); + } + + function setAutomationFeePerSec(AutomationCycleInfo storage cycle, uint128 fee) internal { + cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle &= MAX_UINT128; + cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle |= uint256(fee) << 128; + } + + function setGasCommittedForNewCycle(AutomationCycleInfo storage cycle, uint128 gas) internal { + cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle &= MAX_UINT128 << 128; + cycle.transitionState.automationFeePerSec_gasCommittedForNewCycle |= uint256(gas); + } + + + // gasCommittedForNextCycle (uint128) | sysGasCommittedForNextCycle (uint128) + function gasCommittedForNextCycle(AutomationCycleInfo storage cycle) internal view returns (uint128) { + return uint128(cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle >> 128); + } + + function sysGasCommittedForNextCycle(AutomationCycleInfo storage cycle) internal view returns (uint128) { + return uint128(cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle); + } + + function setGasCommittedForNextCycle(AutomationCycleInfo storage cycle, uint128 gas) internal { + cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle &= MAX_UINT128; + cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle |= uint256(gas) << 128; + } + + function setSysGasCommittedForNextCycle(AutomationCycleInfo storage cycle, uint128 sysGas) internal { + cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle &= MAX_UINT128 << 128; + cycle.transitionState.gasCommittedForNextCycle_sysGasCommittedForNextCycle |= uint256(sysGas); + } + + // refundDuration (uint64) | newCycleDuration (uint64) | nextTaskIndexPosition (uint64) + function refundDuration(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.transitionState.refundDuration_newCycleDuration_nextTaskIndexPosition >> 192); + } + + function newCycleDuration(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.transitionState.refundDuration_newCycleDuration_nextTaskIndexPosition >> 128); + } + + function nextTaskIndexPosition(AutomationCycleInfo storage cycle) internal view returns (uint64) { + return uint64(cycle.transitionState.refundDuration_newCycleDuration_nextTaskIndexPosition >> 64); + } + + function setRefundDuration(AutomationCycleInfo storage cycle, uint64 refund) internal { + TransitionState storage ts = cycle.transitionState; + + // clear bits 192–255 (upper 64 bits) + ts.refundDuration_newCycleDuration_nextTaskIndexPosition &= ~(MAX_UINT64 << 192); + ts.refundDuration_newCycleDuration_nextTaskIndexPosition |= uint256(refund) << 192; + } + + function setNewCycleDuration(AutomationCycleInfo storage cycle, uint64 duration) internal { + TransitionState storage ts = cycle.transitionState; + + // clear bits 128-191 + ts.refundDuration_newCycleDuration_nextTaskIndexPosition &= ~(MAX_UINT64 << 128); + ts.refundDuration_newCycleDuration_nextTaskIndexPosition |= uint256(duration) << 128; + } + + function setNextTaskIndexPosition(AutomationCycleInfo storage cycle, uint64 pos) internal { + TransitionState storage ts = cycle.transitionState; + + // clear bits 64-127 + ts.refundDuration_newCycleDuration_nextTaskIndexPosition &= ~(MAX_UINT64 << 64); + ts.refundDuration_newCycleDuration_nextTaskIndexPosition |= uint256(pos) << 64; + } + + /// @notice Represents intermediate state of the registry on cycle change. + struct IntermediateStateOfCycleChange { + uint256 cycleLockedFees; + uint128 gasCommittedForNextCycle; + uint128 sysGasCommittedForNextCycle; + uint64[] removedTasks; + } + + /// @notice Struct representing transition result. + struct TransitionResult { + uint128 fees; + uint128 gas; + uint128 sysGas; + bool isRemoved; + } + + /// @notice Helper function to sort an array. + /// @param arr Input array to sort. + /// @return Returns the sorted array. + function sortUint64(uint64[] memory arr) internal pure returns (uint64[] memory) { + uint256 length = arr.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = 0; j < length - 1; j++) { + if (arr[j] > arr[j + 1]) { + uint64 temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } + return arr; + } + + /// @notice Helper function to sort an array. + /// @param arr Input array to sort. + /// @return Returns the sorted array. + function sortUint256(uint256[] memory arr) internal pure returns (uint256[] memory) { + uint256 length = arr.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = 0; j < length - 1; j++) { + if (arr[j] > arr[j + 1]) { + uint256 temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } + return arr; + } +} diff --git a/solidity/supra_contracts/src/LibRegistry.sol b/solidity/supra_contracts/src/LibRegistry.sol new file mode 100644 index 0000000000..c0efccdb28 --- /dev/null +++ b/solidity/supra_contracts/src/LibRegistry.sol @@ -0,0 +1,207 @@ + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {EnumerableSet} from "../lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {CommonUtils} from "./CommonUtils.sol"; + +// Helper library used by AutomationRegistry. +library LibRegistry { + + uint256 private constant MAX_UINT128 = type(uint128).max; + uint256 private constant MAX_UINT160 = type(uint160).max; + uint256 private constant MAX_UINT64 = type(uint64).max; + uint256 private constant MAX_UINT8 = type(uint8).max; + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: TaskMetadata :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Task metadata for individual automation tasks. + struct TaskMetadata { + // uint128 | uint128 + uint256 maxGasAmount_gasPriceCap; + + // uint128 | uint128 + uint256 automationFeeCapForCycle_depositFee; + + bytes32 txHash; + + // uint64 | uint64 | uint64 | uint64 + uint256 taskIndex_registrationTime_expiryTime_priority; + + // address | TaskType (uint8) | TaskState (uint8) + uint256 owner_type_state; + + bytes payloadTx; + bytes[] auxData; + } + + function createTaskMetadata( + uint128 _maxGasAmount, + uint128 _gasPriceCap, + uint128 _automationFeeCapForCycle, + uint128 _depositFee, + bytes32 _txHash, + uint64 _taskIndex, + uint64 _registrationTime, + uint64 _expiryTime, + uint64 _priority, + address _owner, + CommonUtils.TaskType _type, + CommonUtils.TaskState _state, + bytes memory _payloadTx, + bytes[] memory _auxData + ) internal pure returns (TaskMetadata memory t) { + // Pack (uint128 | uint128) + t.maxGasAmount_gasPriceCap = (uint256(_maxGasAmount) << 128) | uint256(_gasPriceCap); + + // Pack (uint128 | uint128) + t.automationFeeCapForCycle_depositFee = (uint256(_automationFeeCapForCycle) << 128) | uint256(_depositFee); + + // Direct fields + t.txHash = _txHash; + t.payloadTx = _payloadTx; + t.auxData = _auxData; + + // Pack (uint64 | uint64 | uint64 | uint64) + // Layout: [taskIndex | registrationTime | expiryTime | priority] + t.taskIndex_registrationTime_expiryTime_priority = + (uint256(_taskIndex) << 192) | + (uint256(_registrationTime) << 128) | + (uint256(_expiryTime) << 64) | + uint256(_priority); + + // Pack (address | uint8 | uint8) + // Layout: [owner | taskType | taskState] + t.owner_type_state = + (uint256(uint160(_owner)) << 96) | + (uint256(uint8(_type)) << 88) | + (uint256(uint8(_state))<< 80); + } + + // maxGasAmount (uint128) | gasPriceCap (uint128) + function maxGasAmount(TaskMetadata storage t) internal view returns (uint128) { + return uint128(t.maxGasAmount_gasPriceCap >> 128); + } + + function gasPriceCap(TaskMetadata storage t) internal view returns (uint128) { + return uint128(t.maxGasAmount_gasPriceCap); + } + + function setMaxGasAmount(TaskMetadata storage t, uint128 _value) internal { + t.maxGasAmount_gasPriceCap &= MAX_UINT128; // clear upper 128 + t.maxGasAmount_gasPriceCap |= uint256(_value) << 128; // insert upper 128 + } + + function setGasPriceCap(TaskMetadata storage t, uint128 _value) internal { + t.maxGasAmount_gasPriceCap &= (MAX_UINT128 << 128); // clear lower 128 + t.maxGasAmount_gasPriceCap |= uint256(_value); // insert lower 128 + } + + // automationFeeCapForCycle (uint128) | depositFee (uint128) + function automationFeeCapForCycle(TaskMetadata storage t) internal view returns (uint128) { + return uint128(t.automationFeeCapForCycle_depositFee >> 128); + } + + function depositFee(TaskMetadata storage t) internal view returns (uint128) { + return uint128(t.automationFeeCapForCycle_depositFee); + } + + function setAutomationFeeCapForCycle(TaskMetadata storage t, uint128 _value) internal { + t.automationFeeCapForCycle_depositFee &= MAX_UINT128; + t.automationFeeCapForCycle_depositFee |= uint256(_value) << 128; + } + + function setDepositFee(TaskMetadata storage t, uint128 _value) internal { + t.automationFeeCapForCycle_depositFee &= (MAX_UINT128 << 128); + t.automationFeeCapForCycle_depositFee |= uint256(_value); + } + + // taskIndex (uint64) | registrationTime (uint64) | expiryTime (uint64) | priority (uint64) + function taskIndex(TaskMetadata storage t) internal view returns (uint64) { + return uint64(t.taskIndex_registrationTime_expiryTime_priority >> 192); + } + + function registrationTime(TaskMetadata storage t) internal view returns (uint64) { + return uint64(t.taskIndex_registrationTime_expiryTime_priority >> 128); + } + + function expiryTime(TaskMetadata storage t) internal view returns (uint64) { + return uint64(t.taskIndex_registrationTime_expiryTime_priority >> 64); + } + + function priority(TaskMetadata storage t) internal view returns (uint64) { + return uint64(t.taskIndex_registrationTime_expiryTime_priority); + } + + function setTaskIndex(TaskMetadata storage t, uint64 _value) internal { + t.taskIndex_registrationTime_expiryTime_priority &= ~(MAX_UINT64 << 192); + t.taskIndex_registrationTime_expiryTime_priority |= uint256(_value) << 192; + } + + function setRegistrationTime(TaskMetadata storage t, uint64 _value) internal { + t.taskIndex_registrationTime_expiryTime_priority &= ~(MAX_UINT64 << 128); + t.taskIndex_registrationTime_expiryTime_priority |= uint256(_value) << 128; + } + + function setExpiryTime(TaskMetadata storage t, uint64 _value) internal { + t.taskIndex_registrationTime_expiryTime_priority &= ~(MAX_UINT64 << 64); + t.taskIndex_registrationTime_expiryTime_priority |= uint256(_value) << 64; + } + + function setPriority(TaskMetadata storage t, uint64 _value) internal { + t.taskIndex_registrationTime_expiryTime_priority &= ~MAX_UINT64; + t.taskIndex_registrationTime_expiryTime_priority |= uint256(_value); + } + + // owner (address/uint160) | type (TaskType/uint8) | state (TaskState/uint8) + function owner(TaskMetadata storage t) internal view returns (address) { + return address(uint160(t.owner_type_state >> 96)); + } + + function taskType(TaskMetadata storage t) internal view returns (CommonUtils.TaskType) { + return CommonUtils.TaskType(uint8(t.owner_type_state >> 88)); + } + + function state(TaskMetadata storage t) internal view returns (CommonUtils.TaskState) { + return CommonUtils.TaskState(uint8(t.owner_type_state >> 80)); + } + + function setOwner(TaskMetadata storage t, address _value) internal { + t.owner_type_state &= ~(MAX_UINT160 << 96); + t.owner_type_state |= uint256(uint160(_value)) << 96; + } + + function setType(TaskMetadata storage t, uint8 _value) internal { + t.owner_type_state &= ~(MAX_UINT8 << 88); + t.owner_type_state |= uint256(_value) << 88; + } + + function setState(TaskMetadata storage t, uint8 _value) internal { + t.owner_type_state &= ~(MAX_UINT8 << 80); + t.owner_type_state |= uint256(_value) << 80; + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: RegistryState :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @notice Tracks per-cycle automation state and task indexes. + struct RegistryState { + uint64 currentIndex; + + EnumerableSet.UintSet activeTaskIds; + EnumerableSet.UintSet taskIdList; + mapping(uint64 => TaskMetadata) tasks; + // mapping(address => uint64[]) userTasks TO_DO: user to their tasks, need to decide on this + + EnumerableSet.UintSet sysTaskIds; + EnumerableSet.AddressSet authorizedAccounts; + } + + /// @notice Struct representing a stopped task. + struct TaskStopped { + uint64 taskIndex; + uint128 depositRefund; + uint128 cycleFeeRefund; + bytes32 txHash; + } +} + diff --git a/solidity/supra_contracts/test/AutomationController.t.sol b/solidity/supra_contracts/test/AutomationController.t.sol new file mode 100644 index 0000000000..21c4db7ddb --- /dev/null +++ b/solidity/supra_contracts/test/AutomationController.t.sol @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from"../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {AutomationRegistry} from "../src/AutomationRegistry.sol"; +import {AutomationCore} from "../src/AutomationCore.sol"; +import {AutomationController} from "../src/AutomationController.sol"; +import {IAutomationController} from "../src/IAutomationController.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {CommonUtils} from "../src/CommonUtils.sol"; +import {LibConfig} from "../src/LibConfig.sol"; + +contract AutomationControllerTest is Test { + ERC20Supra erc20Supra; // ERC20Supra contract + AutomationCore automationCore; // AutomationCore instance on proxy address + AutomationRegistry registry; // AutomationRegistry instance on proxy address + AutomationController controller; // AutomationController instance on proxy address + + /// @dev Address of the transaction hash precompile. + address constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + address admin = address(0xA11CE); + address vmSigner = address(0x53555000); + address alice = address(0x123); + address bob = address(0x456); + + /// @dev Sets up initial state for testing. + /// @dev Sets balance of 'alice' to 100 ether. + /// @dev Deploys and initializes all contracts with required parameters. + function setUp() public { + vm.deal(alice, 100 ether); + + vm.startPrank(admin); + erc20Supra = new ERC20Supra(msg.sender); + + AutomationCore automationCoreImpl = new AutomationCore(); + bytes memory automationCoreInitData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, // taskDurationCapSecs + 10_000_000, // registryMaxGasCap + 0.001 ether, // automationBaseFeeWeiPerSec + 0.002 ether, // flatRegistrationFeeWei + 50, // congestionThresholdPercentage + 0.002 ether, // congestionBaseFeeWeiPerSec + 2, // congestionExponent + 500, // taskCapacity + 2000, // cycleDurationSecs + 3600, // sysTaskDurationCapSecs + 5_000_000, // sysRegistryMaxGasCap + 500, // sysTaskCapacity + vmSigner, // VM Signer address + address(erc20Supra) // ERC20Supra address + ) + ); + ERC1967Proxy automationCoreProxy = new ERC1967Proxy(address(automationCoreImpl), automationCoreInitData); + automationCore = AutomationCore(address(automationCoreProxy)); + + AutomationRegistry registryImpl = new AutomationRegistry(); + bytes memory registryInitData = abi.encodeCall(AutomationRegistry.initialize, (address(automationCore))); + ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInitData); + registry = AutomationRegistry(address(registryProxy)); + + AutomationController controllerImpl = new AutomationController(); + bytes memory controllerInitData = abi.encodeCall(AutomationController.initialize,(address(automationCore), address(registry), true)); + ERC1967Proxy controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + controller = AutomationController(address(controllerProxy)); + + automationCore.setAutomationRegistry(address(registry)); + automationCore.setAutomationController(address(controller)); + registry.setAutomationController(address(controller)); + + vm.stopPrank(); + + vm.mockCall( + TX_HASH_PRECOMPILE, + bytes(""), + abi.encode(keccak256("txHash")) + ); + } + + /// @dev Test to ensure all state variables are initialized correctly. + function testInitialize() public view { + assertEq(controller.owner(), admin); + assertEq(address(controller.automationCore()), address(automationCore)); + assertEq(address(controller.registry()), address(registry)); + assertTrue(controller.isAutomationEnabled()); + } + + /// @dev Test to ensure initialize reverts if reinitialized. + function testInitializeRevertsIfReinitialized() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + + vm.prank(admin); + controller.initialize(address(automationCore), address(registry), true); + } + + /// @dev Test to ensure initialize reverts if AutomationCore address is zero. + function testInitializeRevertsIfAutomationCoreAddressZero() public { + AutomationController impl = new AutomationController(); + bytes memory initData = abi.encodeCall(AutomationController.initialize, (address(0), address(registry), true)); + + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + new ERC1967Proxy(address(impl), initData); + } + + /// @dev Test to ensure initialize reverts if AutomationCore address is EOA. + function testInitializeRevertsIfAutomationCoreEoa() public { + AutomationController impl = new AutomationController(); + bytes memory initData = abi.encodeCall(AutomationController.initialize, (alice, address(registry), true)); + + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + new ERC1967Proxy(address(impl), initData); + } + + /// @dev Test to ensure initialize reverts if AutomationRegistry address is zero. + function testInitializeRevertsIfRegistryZero() public { + AutomationController impl = new AutomationController(); + bytes memory initData = abi.encodeCall(AutomationController.initialize, (address(automationCore), address(0), true)); + + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + new ERC1967Proxy(address(impl), initData); + } + + /// @dev Test to ensure initialize reverts if AutomationRegistry address is EOA. + function testInitializeRevertsIfRegistryEoa() public { + AutomationController impl = new AutomationController(); + bytes memory initData = abi.encodeCall(AutomationController.initialize, (address(automationCore), alice, true)); + + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + new ERC1967Proxy(address(impl), initData); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if caller is not owner. + function testSetAutomationRegistryRevertsIfNotOwner() public { + AutomationRegistry registryImplementation = new AutomationRegistry(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + controller.setAutomationRegistry(address(registryImplementation)); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if address is zero. + function testSetAutomationRegistryRevertsIfAddressZero() public { + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + + vm.prank(admin); + controller.setAutomationRegistry(address(0)); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if address is EOA. + function testSetAutomationRegistryRevertsIfAddressEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + controller.setAutomationRegistry(alice); + } + + /// @dev Test to ensure 'setAutomationRegistry' updates the registry address. + function testSetAutomationRegistry() public { + AutomationRegistry registryImplementation = new AutomationRegistry(); + + vm.prank(admin); + controller.setAutomationRegistry(address(registryImplementation)); + + assertEq(address(controller.registry()), address(registryImplementation)); + } + + /// @dev Test to ensure 'setAutomationRegistry' emits event 'AutomationRegistryUpdated'. + function testSetAutomationRegistryEmitsEvent() public { + AutomationRegistry registryImplementation = new AutomationRegistry(); + + vm.expectEmit(true, true, false, false); + emit AutomationController.AutomationRegistryUpdated(address(controller.registry()), address(registryImplementation)); + + vm.prank(admin); + controller.setAutomationRegistry(address(registryImplementation)); + } + + /// @dev Test to ensure 'setAutomationCore' reverts if caller is not owner. + function testSetAutomationCoreRevertsIfNotOwner() public { + AutomationCore automationCoreImpl = new AutomationCore(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + controller.setAutomationCore(address(automationCoreImpl)); + } + + /// @dev Test to ensure 'setAutomationCore' reverts if address is zero. + function testSetAutomationCoreRevertsIfAddressZero() public { + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + + vm.prank(admin); + controller.setAutomationCore(address(0)); + } + + /// @dev Test to ensure 'setAutomationCore' reverts if address is EOA. + function testSetAutomationCoreRevertsIfAddressEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + controller.setAutomationCore(alice); + } + + /// @dev Test to ensure 'setAutomationCore' updates the AutomationCore address. + function testSetAutomationCore() public { + AutomationCore automationCoreImpl = new AutomationCore(); + + vm.prank(admin); + controller.setAutomationCore(address(automationCoreImpl)); + + assertEq(address(controller.automationCore()), address(automationCoreImpl)); + } + + /// @dev Test to ensure 'setAutomationCore' emits event 'AutomationCoreUpdated'. + function testSetAutomationCoreEmitsEvent() public { + AutomationCore automationCoreImpl = new AutomationCore(); + + vm.expectEmit(true, true, false, false); + emit AutomationController.AutomationCoreUpdated(address(controller.automationCore()), address(automationCoreImpl)); + + vm.prank(admin); + controller.setAutomationCore(address(automationCoreImpl)); + } + + /// @dev Test to ensure 'monitorCycleEnd' reverts if tx.origin is not VM Signer. + function testMonitorCycleEndRevertsIfTxOriginNotVm() public { + vm.expectRevert(IAutomationController.CallerNotVmSigner.selector); + + vm.prank(vmSigner); + controller.monitorCycleEnd(); + } + + /// @dev Test to ensure 'monitorCycleEnd' does nothing before cycle expiry. + function testMonitorCycleEndDoesNothingBeforeCycleExpiry() public { + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(stateBefore)); + } + + // /// @dev Test to ensure 'monitorCycleEnd' does nothing if state is not STARTED. + // function testMonitorCycleEndDoesNothingIfNotStarted() public { + // // Move state to READY state + // vm.prank(address(automationCore)); + // controller.tryMoveToSuspendedState(); + + // (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + // assertEq(uint8(stateBefore), uint8(CommonUtils.CycleState.READY)); + + // vm.warp(startBefore + durationBefore); + + // vm.prank(vmSigner, vmSigner); + // controller.monitorCycleEnd(); + + // (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + + // assertEq(indexAfter, indexBefore); + // assertEq(startAfter, startBefore); + // assertEq(durationAfter, durationBefore); + // assertEq(uint8(stateAfter), uint8(stateBefore)); + // } + + /// @dev Test to ensure 'monitorCycleEnd' moves cycle state to READY if automation is disabled and no tasks exist. + function testMonitorCycleEndWhenAutomationDisabledNoTasks() public { + // Disable automation + vm.prank(admin); + controller.disableAutomation(); + + assertFalse(controller.isAutomationEnabled()); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, false, true); + emit AutomationController.AutomationCycleEvent( + indexBefore, + CommonUtils.CycleState.READY, + startBefore, + durationBefore, + stateBefore + ); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.READY)); + } + + /// @dev Test to ensure 'monitorCycleEnd' moves cycle state to STARTED if automation is enabled and no tasks exist. + function testMonitorCycleEndWhenAutomationEnabledNoTasks() public { + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, false, true); + emit AutomationController.AutomationCycleEvent( + indexBefore + 1, + CommonUtils.CycleState.STARTED, + uint64(block.timestamp), + durationBefore, + stateBefore + ); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + + assertEq(indexAfter, indexBefore + 1); + assertEq(startAfter, block.timestamp); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.STARTED)); + } + + /// @dev Test to ensure 'monitorCycleEnd' moves cycle state to FINISHED if automation is enabled and tasks exist. + function testMonitorCycleEndWhenAutomationEnabledAndTasksExist() public { + registerTask(); + + (uint64 indexBefore, uint64 startBefore, uint64 durationBefore, CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + vm.warp(startBefore + durationBefore); + + vm.expectEmit(true, true, false, true); + emit AutomationController.AutomationCycleEvent( + indexBefore, + CommonUtils.CycleState.FINISHED, + startBefore, + durationBefore, + stateBefore + ); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 indexAfter, uint64 startAfter, uint64 durationAfter, CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + + assertEq(indexAfter, indexBefore); + assertEq(startAfter, startBefore); + assertEq(durationAfter, durationBefore); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.FINISHED)); + + (uint64 refundDuration, uint128 automationFeePerSec) = controller.getTransitionInfo(); + assertEq(refundDuration, 0); + assertEq(automationFeePerSec, 1000000000000000); + } + + /// @dev Test to ensure 'processTasks' reverts if caller is not VM Signer. + function testProcessTasksRevertsIfNotVm() public { + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectRevert(IAutomationController.CallerNotVmSigner.selector); + + vm.prank(admin); + controller.processTasks(1, tasks); + } + + /// @dev Test to ensure 'processTasks' reverts if state is not FINISHED or SUSPENDED. + function testProcessTasksRevertsIfInvalidState() public { + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectRevert(IAutomationController.InvalidRegistryState.selector); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(1, tasks); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is FINISHED. + function testProcessTasksWhenCycleStateFinished() public { + registerTask(); + + ( , uint64 startTime, uint64 duration, ) = controller.getCycleInfo(); + vm.warp(startTime + duration); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 index, , , CommonUtils.CycleState state) = controller.getCycleInfo(); + assertEq(uint8(state), uint8(CommonUtils.CycleState.FINISHED)); + + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + uint256[] memory activeTasks = new uint256[](1); + tasks[0] = 0; + + vm.expectEmit(true, false, false, false); + emit AutomationController.ActiveTasks(activeTasks); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(index + 1, tasks); + + (uint64 newIndex, uint64 newStart, uint64 newDuration, CommonUtils.CycleState newState) = controller.getCycleInfo(); + assertEq(newIndex, index + 1); + assertEq(newStart, uint64(block.timestamp)); + assertEq(newDuration, 2000); + assertEq(uint8(newState), uint8(CommonUtils.CycleState.STARTED)); + + assertEq(registry.getAllActiveTaskIds(), activeTasks); + assertEq(automationCore.getSystemGasCommittedForNextCycle(), 0); + assertEq(automationCore.getSystemGasCommittedForCurrentCycle(), 0); + assertEq(automationCore.getGasCommittedForNextCycle(), 0); + assertEq(automationCore.getGasCommittedForCurrentCycle(), 1000000); + assertEq(automationCore.getCycleLockedFees(), 200000000000000000); + } + + /// @dev Test to ensure 'processTasks' reverts if invalid cycle index is passed when cycle state is FINISHED. + function testProcessTasksRevertsIfInvalidCycleIndexWhenCycleStateFinished() public { + registerTask(); + + ( , uint64 startTime, uint64 duration, ) = controller.getCycleInfo(); + vm.warp(startTime + duration); + + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + (uint64 index, , , CommonUtils.CycleState state) = controller.getCycleInfo(); + assertEq(uint8(state), uint8(CommonUtils.CycleState.FINISHED)); + + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectRevert(IAutomationController.InvalidInputCycleIndex.selector); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(index, tasks); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is disabled. + function testProcessTasksWhenCycleStateSuspendedAutomationDisabled() public { + registerTask(); + + ( , uint64 start, uint64 duration, ) = controller.getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + ( , , , CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + assertEq(uint8(stateBefore), uint8(CommonUtils.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + controller.disableAutomation(); + + (uint64 indexAfter, , , CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.SUSPENDED)); + + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectEmit(true, false, false, false); + emit AutomationController.RemovedTasks(tasks); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(indexAfter, tasks); + + ( , , , CommonUtils.CycleState newState) = controller.getCycleInfo(); + assertEq(uint8(newState), uint8(CommonUtils.CycleState.READY)); + assertFalse(registry.ifTaskExists(tasks[0])); + } + + /// @dev Test to ensure 'processTasks' works correctly when cycle state is SUSPENDED and automation is enabled. + function testProcessTasksWhenCycleStateSuspendedAutomationEnabled() public { + registerTask(); + + ( , uint64 start, uint64 duration, ) = controller.getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + ( , , , CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + assertEq(uint8(stateBefore), uint8(CommonUtils.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + controller.disableAutomation(); + + (uint64 indexAfter, , , CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.SUSPENDED)); + + // Enable automation + vm.prank(admin); + controller.enableAutomation(); + + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectEmit(true, false, false, false); + emit AutomationController.RemovedTasks(tasks); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(indexAfter, tasks); + + (uint64 newIndex, uint64 newStart, uint64 newDuration, CommonUtils.CycleState newState) = controller.getCycleInfo(); + assertEq(newIndex, indexAfter + 1); + assertEq(newStart, uint64(block.timestamp)); + assertEq(newDuration, 2000); + assertEq(uint8(newState), uint8(CommonUtils.CycleState.STARTED)); + assertFalse(registry.ifTaskExists(tasks[0])); + } + + /// @dev Test to ensure 'processTasks' reverts if invalid cycle index is passed when cycle state is SUSPENDED. + function testProcessTasksRevertsIfInvalidCycleIndexWhenCycleStateSuspended() public { + registerTask(); + + ( , uint64 start, uint64 duration, ) = controller.getCycleInfo(); + vm.warp(start + duration); + + // Moves state to FINISHED + vm.prank(vmSigner, vmSigner); + controller.monitorCycleEnd(); + + ( , , , CommonUtils.CycleState stateBefore) = controller.getCycleInfo(); + assertEq(uint8(stateBefore), uint8(CommonUtils.CycleState.FINISHED)); + + // Disable automation → moves state to SUSPENDED + vm.prank(admin); + controller.disableAutomation(); + + (uint64 indexAfter, , , CommonUtils.CycleState stateAfter) = controller.getCycleInfo(); + assertEq(uint8(stateAfter), uint8(CommonUtils.CycleState.SUSPENDED)); + + uint64[] memory tasks = new uint64[](1); + tasks[0] = 0; + + vm.expectRevert(IAutomationController.InvalidInputCycleIndex.selector); + + vm.prank(vmSigner, vmSigner); + controller.processTasks(indexAfter + 1, tasks); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'disableAutomation' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'disableAutomation' disables the automation. + function testDisableAutomation() public { + // Already enabled in initialize() + vm.prank(admin); + controller.disableAutomation(); + + assertFalse(controller.isAutomationEnabled()); + } + + /// @dev Test to ensure 'disableAutomation' emits event 'AutomationDisabled'. + function testDisableAutomationEmitsEvent() public { + vm.expectEmit(true, false, false, false); + emit AutomationController.AutomationDisabled(false); + + vm.prank(admin); + controller.disableAutomation(); + } + + /// @dev Test to ensure 'disableAutomation' reverts if automation is already disabled. + function testDisableAutomationRevertsIfAlreadyDisabled() public { + // Disable automation + testDisableAutomation(); + + // Disable again → revert + vm.expectRevert(IAutomationController.AlreadyDisabled.selector); + + vm.prank(admin); + controller.disableAutomation(); + } + + /// @dev Test to ensure 'disableAutomation' reverts if caller is not owner. + function testDisableAutomationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + controller.disableAutomation(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'enableAutomation' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'enableAutomation' enables the automation. + function testEnableAutomation() public { + // Disable automation + testDisableAutomation(); + + // Enable automation + vm.prank(admin); + controller.enableAutomation(); + + assertTrue(controller.isAutomationEnabled()); + } + + /// @dev Test to ensure 'enableAutomation' emits event 'AutomationEnabled'. + function testEnableAutomationEmitsEvent() public { + // Disable automation + testDisableAutomation(); + + vm.expectEmit(true, false, false, false); + emit AutomationController.AutomationEnabled(true); + + vm.prank(admin); + controller.enableAutomation(); + } + + /// @dev Test to ensure 'enableAutomation' reverts if automation is already enabled. + function testEnableAutomationRevertsIfAlreadyEnabled() public { + // Already enabled in initialize() + vm.expectRevert(IAutomationController.AlreadyEnabled.selector); + + vm.prank(admin); + controller.enableAutomation(); + } + + /// @dev Test to ensure 'enableAutomation' reverts if caller is not owner. + function testEnableAutomationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + controller.enableAutomation(); + } + + /// @dev Helper function to register a UST. + function registerTask() private { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.startPrank(alice); + erc20Supra.nativeToErc20Supra{value: 5 ether}(); + erc20Supra.approve(address(automationCore), type(uint256).max); + + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 2, + auxData + ); + vm.stopPrank(); + } + + /// @dev Helper function to return payload. + /// @param _value Value to be sent along with transaction. + /// @param _target Address of destination smart contract. + function createPayload(uint128 _value, address _target) private pure returns (bytes memory) { + LibConfig.AccessListEntry[] memory accessList = new LibConfig.AccessListEntry[](2); + + bytes32[] memory keys = new bytes32[](2); + keys[0] = bytes32(uint256(0)); + keys[1] = bytes32(uint256(1)); + + accessList[0] = LibConfig.AccessListEntry({ + addr: address(0x1111), + storageKeys: keys + }); + + accessList[1] = LibConfig.AccessListEntry({ + addr: address(0x2222), + storageKeys: keys + }); + + bytes memory callData = abi.encodeCall(ERC20Supra.erc20SupraToNative, 100); + bytes memory payload = abi.encode(_value, _target, callData, accessList); + + return payload; + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/AutomationCore.t.sol b/solidity/supra_contracts/test/AutomationCore.t.sol new file mode 100644 index 0000000000..2350735502 --- /dev/null +++ b/solidity/supra_contracts/test/AutomationCore.t.sol @@ -0,0 +1,945 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {AutomationCore} from "../src/AutomationCore.sol"; +import {AutomationController} from "../src/AutomationController.sol"; +import {AutomationRegistry} from "../src/AutomationRegistry.sol"; +import {IAutomationCore} from "../src/IAutomationCore.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {CommonUtils} from "../src/CommonUtils.sol"; +import {LibConfig} from "../src/LibConfig.sol"; + +contract AutomationCoreTest is Test { + ERC20Supra erc20Supra; // ERC20Supra contract + AutomationCore automationCore; // AutomationCore instance on proxy address + AutomationRegistry registry; // AutomationRegistry instance on proxy address + AutomationController automationController; // AutomationController instance on proxy address + + /// @dev Address of the transaction hash precompile. + address constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + address admin = address(0xA11CE); + address vmSigner = address(0x53555000); + address alice = address(0x123); + address bob = address(0x456); + + /// @dev Sets up initial state for testing. + /// @dev Sets balance of 'alice' to 100 ether. + /// @dev Deploys and initializes all contracts with required parameters. + function setUp() public { + vm.deal(alice, 100 ether); + + vm.startPrank(admin); + erc20Supra = new ERC20Supra(msg.sender); + + AutomationCore automationCoreImpl = new AutomationCore(); + bytes memory automationCoreInitData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, // taskDurationCapSecs + 10_000_000, // registryMaxGasCap + 0.001 ether, // automationBaseFeeWeiPerSec + 0.002 ether, // flatRegistrationFeeWei + 50, // congestionThresholdPercentage + 0.002 ether, // congestionBaseFeeWeiPerSec + 2, // congestionExponent + 500, // taskCapacity + 2000, // cycleDurationSecs + 3600, // sysTaskDurationCapSecs + 5_000_000, // sysRegistryMaxGasCap + 500, // sysTaskCapacity + vmSigner, // VM Signer address + address(erc20Supra) // ERC20Supra address + ) + ); + ERC1967Proxy automationCoreProxy = new ERC1967Proxy(address(automationCoreImpl), automationCoreInitData); + automationCore = AutomationCore(address(automationCoreProxy)); + + AutomationRegistry registryImpl = new AutomationRegistry(); + bytes memory registryInitData = abi.encodeCall(AutomationRegistry.initialize, (address(automationCore))); + ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInitData); + registry = AutomationRegistry(address(registryProxy)); + + AutomationController controllerImpl = new AutomationController(); + bytes memory controllerInitData = abi.encodeCall(AutomationController.initialize,(address(automationCore), address(registry), true)); + ERC1967Proxy controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + automationController = AutomationController(address(controllerProxy)); + + automationCore.setAutomationRegistry(address(registry)); + automationCore.setAutomationController(address(automationController)); + registry.setAutomationController(address(automationController)); + + vm.stopPrank(); + + vm.mockCall( + TX_HASH_PRECOMPILE, + bytes(""), + abi.encode(keccak256("txHash")) + ); + } + + /// @dev Test to ensure all state variables are initialized correctly. + function testInitialize() public view { + assertEq(automationCore.owner(), admin); + + (uint64 index, uint64 startTime, uint64 durationSecs, CommonUtils.CycleState state) = automationController.getCycleInfo(); + assertEq(index, 1); + assertEq(startTime, block.timestamp); + assertEq(durationSecs, 2000); + assertEq(uint8(state), uint8(CommonUtils.CycleState.STARTED)); + + assertEq(automationCore.getNextCycleRegistryMaxGasCap(), 10_000_000); + assertEq(automationCore.getNextCycleSysRegistryMaxGasCap(), 5_000_000); + assertEq(automationCore.getAutomationController(), address(automationController)); + assertTrue(automationCore.isRegistrationEnabled()); + assertTrue(automationController.isAutomationEnabled()); + assertEq(automationCore.getVmSigner(), vmSigner); + assertEq(automationCore.erc20Supra(), address(erc20Supra)); + + LibConfig.ConfigDetails memory config = automationCore.getConfig(); + + assertEq(config.registryMaxGasCap, 10_000_000); + assertEq(config.sysRegistryMaxGasCap, 5_000_000); + assertEq(config.automationBaseFeeWeiPerSec, 0.001 ether); + assertEq(config.flatRegistrationFeeWei, 0.002 ether); + assertEq(config.congestionBaseFeeWeiPerSec, 0.002 ether); + assertEq(config.taskDurationCapSecs, 3600); + assertEq(config.sysTaskDurationCapSecs, 3600); + assertEq(config.cycleDurationSecs, 2000); + assertEq(config.taskCapacity, 500); + assertEq(config.sysTaskCapacity, 500); + assertEq(config.congestionThresholdPercentage, 50); + assertEq(config.congestionExponent, 2); + } + + /// @dev Test to ensure reinitialization fails. + function testInitializeRevertsIfReinitialized() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + + vm.prank(admin); + automationCore.initialize( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, + 500, 2000, 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ); + } + + /// @dev Test to ensure initialization fails if zero address is passed as VM Signer. + function testInitializeRevertsIfVmSignerZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, + 2, 500, 2000, 3600, 5_000_000, 500, + address(0), // VM Signer as zero + address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if ERC20Supra address is zero. + function testInitializeRevertsIfErc20SupraIsZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, + 2, 500, 2000, 3600, 5_000_000, 500, vmSigner, + address(0) // address(0) as ERC20Supra + ) + ); + + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if EOA is passed as ERC20Supra address. + function testInitializeRevertsIfErc20SupraIsEoa() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, + 2, 500, 2000, 3600, 5_000_000, 500, vmSigner, + admin // EOA address as ERC20Supra + ) + ); + + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if task duration is <= cycle duration. + function testInitializeRevertsIfInvalidTaskDuration() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 2000, // task duration + 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 500, + 2000, // cycle duration + 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidTaskDuration.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if registry max gas cap is zero. + function testInitializeRevertsIfRegistryMaxGasCapZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, + 0, // registry max gas cap + 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 500, + 2000, 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidRegistryMaxGasCap.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if congestion threshold percentage is > 100. + function testInitializeRevertsIfInvalidCongestionThreshold() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, + 101, // congestion threshold percentage > 100 + 0.002 ether, 2, 500, 2000, 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidCongestionThreshold.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if congestion exponent is 0. + function testInitializeRevertsIfCongestionExponentZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, + 0, // congestion exponent + 500, 2000, 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidCongestionExponent.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if task capacity is 0. + function testInitializeRevertsIfTaskCapacityZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, + 0, // 0 as task capacity + 2000, 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidTaskCapacity.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if cycle duration is 0. + function testInitializeRevertsIfCycleDurationZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 500, + 0, // cycle duration + 3600, 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidCycleDuration.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if system task duration is <= cycle duration. + function testInitializeRevertsIfInvalidSysTaskDuration() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 500, + 2000, // cycle duration + 2000, // system task duration + 5_000_000, 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidSysTaskDuration.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if system registry max gas cap is 0. + function testInitializeRevertsIfSysRegistryMaxGasCapZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, 2, 500, 2000, 3600, + 0, // system registry max gas cap + 500, vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidSysRegistryMaxGasCap.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if system task capacity is 0. + function testInitializeRevertsIfSysTaskCapacityZero() public { + AutomationCore implementation = new AutomationCore(); + + bytes memory initData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, 10_000_000, 0.001 ether, 0.002 ether, 50, 0.002 ether, + 2, 500, 2000, 3600, 5_000_000, + 0, // system task capacity + vmSigner, address(erc20Supra) + ) + ); + + vm.expectRevert(IAutomationCore.InvalidSysTaskCapacity.selector); + new ERC1967Proxy(address(implementation), initData); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'disableRegistration' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'disableRegistration' disables the registration. + function testDisableRegistration() public { + vm.prank(admin); + automationCore.disableRegistration(); + + assertFalse(automationCore.isRegistrationEnabled()); + } + + /// @dev Test to ensure 'disableRegistration' emits event 'TaskRegistrationDisabled'. + function testDisableRegistrationEmitsEvent() public { + vm.expectEmit(true, false, false, false); + emit AutomationCore.TaskRegistrationDisabled(false); + + testDisableRegistration(); + } + + /// @dev Test to ensure 'disableRegistration' reverts if registration is already disabled. + function testDisableRegistrationRevertsIfAlreadyDisabled() public { + // Disable registration + testDisableRegistration(); + + // Disable again → revert + vm.expectRevert(IAutomationCore.AlreadyDisabled.selector); + + vm.prank(admin); + automationCore.disableRegistration(); + } + + /// @dev Test to ensure 'disableRegistration' reverts if caller is not owner. + function testDisableRegistrationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + automationCore.disableRegistration(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'enableRegistration' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'enableRegistration' enables the registration. + function testEnableRegistration() public { + // Disable registration + testDisableRegistration(); + + // Enable registration + vm.prank(admin); + automationCore.enableRegistration(); + + assertTrue(automationCore.isRegistrationEnabled()); + } + + /// @dev Test to ensure 'enableRegistration' emits event 'TaskRegistrationEnabled'. + function testEnableRegistrationEmitsEvent() public { + // Disable registration + testDisableRegistration(); + + vm.expectEmit(true, false, false, false); + emit AutomationCore.TaskRegistrationEnabled(true); + + // Enable registration + vm.prank(admin); + automationCore.enableRegistration(); + } + + /// @dev Test to ensure 'enableRegistration' reverts if registration is already enabled. + function testEnableRegistrationRevertsIfAlreadyEnabled() public { + // Already enabled in initialize() + vm.expectRevert(IAutomationCore.AlreadyEnabled.selector); + + vm.prank(admin); + automationCore.enableRegistration(); + } + + /// @dev Test to ensure 'enableRegistration' reverts if caller is not owner. + function testEnableRegistrationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + automationCore.enableRegistration(); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'setAutomationRegistry' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function that deploys AutomationRegistry and returns its address. + function deployAutomationRegistry() internal returns (address) { + // Deploy AutomationRegistry proxy + AutomationRegistry registryImpl = new AutomationRegistry(); + bytes memory registryInitData = abi.encodeCall(AutomationRegistry.initialize,(address(automationCore))); + ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInitData); + + return address(registryProxy); + } + + /// @dev Test to ensure 'setAutomationRegistry' updates the automation registry address. + function testSetAutomationRegistry() public { + address registryAddr = deployAutomationRegistry(); + + vm.prank(admin); + automationCore.setAutomationRegistry(registryAddr); + + assertEq(automationCore.getAutomationRegistry(), registryAddr); + } + + /// @dev Test to ensure 'setAutomationRegistry' emits event 'AutomationRegistryUpdated'. + function testSetAutomationRegistryEmitsEvent() public { + address oldRegistry = automationCore.getAutomationRegistry(); + address registryAddr = deployAutomationRegistry(); + + vm.expectEmit(true, true, false, false); + emit AutomationCore.AutomationRegistryUpdated(oldRegistry, registryAddr); + + vm.prank(admin); + automationCore.setAutomationRegistry(registryAddr); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if caller is not owner. + function testSetAutomationRegistryRevertsIfNotOwner() public { + address registryAddr = deployAutomationRegistry(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + automationCore.setAutomationRegistry(registryAddr); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if zero address is passed. + function testSetAutomationRegistryRevertsIfZeroAddress() public { + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + + vm.prank(admin); + automationCore.setAutomationRegistry(address(0)); + } + + /// @dev Test to ensure 'setAutomationRegistry' reverts if EOA is passed. + function testSetAutomationRegistryRevertsIfEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + automationCore.setAutomationRegistry(alice); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'setAutomationController' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function that deploys AutomationController and returns its address. + function deployAutomationController() internal returns (address) { + // Deploy AutomationController proxy + AutomationController controllerImpl = new AutomationController(); + bytes memory controllerInitData = abi.encodeCall(AutomationController.initialize,(address(automationCore), address(registry), true)); + ERC1967Proxy controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + + return address(controllerProxy); + } + + /// @dev Test to ensure 'setAutomationController' updates the automation controller address. + function testSetAutomationController() public { + address controller = deployAutomationController(); + + vm.prank(admin); + automationCore.setAutomationController(controller); + + assertEq(automationCore.getAutomationController(), controller); + } + + /// @dev Test to ensure 'setAutomationController' emits event 'AutomationControllerUpdated'. + function testSetAutomationControllerEmitsEvent() public { + address oldController = automationCore.getAutomationController(); + address controller = deployAutomationController(); + + vm.expectEmit(true, true, false, false); + emit AutomationCore.AutomationControllerUpdated(oldController, controller); + + vm.prank(admin); + automationCore.setAutomationController(controller); + } + + /// @dev Test to ensure 'setAutomationController' reverts if caller is not owner. + function testSetAutomationControllerRevertsIfNotOwner() public { + address controller = deployAutomationController(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + automationCore.setAutomationController(controller); + } + + /// @dev Test to ensure 'setAutomationController' reverts if zero address is passed. + function testSetAutomationControllerRevertsIfZeroAddress() public { + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + + vm.prank(admin); + automationCore.setAutomationController(address(0)); + } + + /// @dev Test to ensure 'setAutomationController' reverts if EOA is passed. + function testSetAutomationControllerRevertsIfEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + automationCore.setAutomationController(alice); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'setVmSigner' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'setVmSigner' updates the VM Signer address. + function testSetVmSigner() public { + address newVmSigner = address(0x100); + + vm.prank(admin); + automationCore.setVmSigner(newVmSigner); + + assertEq(automationCore.getVmSigner(), newVmSigner); + } + + /// @dev Test to ensure 'setVmSigner' emits event 'VmSignerUpdated'. + function testSetVmSignerEmitsEvent() public { + address oldVmSigner = automationCore.getVmSigner(); + address newVmSigner = address(0x100); + + vm.expectEmit(true, true, false, false); + emit AutomationCore.VmSignerUpdated(oldVmSigner, newVmSigner); + + vm.prank(admin); + automationCore.setVmSigner(newVmSigner); + } + + /// @dev Test to ensure 'setVmSigner' reverts if zero address is passed. + function testSetVmSignerRevertsIfZeroAddress() public { + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + + vm.prank(admin); + automationCore.setVmSigner(address(0)); + } + + /// @dev Test to ensure 'setVmSigner' reverts if caller is not owner. + function testSetVmSignerRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + automationCore.setVmSigner(address(0x100)); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'setErc20Supra' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'setErc20Supra' updates the ERC20Supra address. + function testSetErc20Supra() public { + ERC20Supra supraErc20 = new ERC20Supra(msg.sender); + + vm.prank(admin); + automationCore.setErc20Supra(address(supraErc20)); + + assertEq(automationCore.erc20Supra(), address(supraErc20)); + } + + /// @dev Test to ensure 'setErc20Supra' emits event 'Erc20SupraUpdated'. + function testSetErc20SupraEmitsEvent() public { + address oldAddr = automationCore.erc20Supra(); + ERC20Supra supraErc20 = new ERC20Supra(msg.sender); + + vm.expectEmit(true, true, false, false); + emit AutomationCore.Erc20SupraUpdated(oldAddr, address(supraErc20)); + + vm.prank(admin); + automationCore.setErc20Supra(address(supraErc20)); + } + + /// @dev Test to ensure 'setErc20Supra' reverts if zero address is passed. + function testSetErc20SupraRevertsIfZeroAddress() public { + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + + vm.prank(admin); + automationCore.setErc20Supra(address(0)); + } + + /// @dev Test to ensure 'setErc20Supra' reverts if EOA is passed. + function testSetErc20SupraRevertsIfEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + automationCore.setErc20Supra(alice); + } + + /// @dev Test to ensure 'setErc20Supra' reverts if caller is not owner. + function testSetErc20SupraRevertsIfNotOwner() public { + ERC20Supra supraErc20 = new ERC20Supra(msg.sender); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + automationCore.setErc20Supra(address(supraErc20)); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'updateConfigBuffer' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function that returns a valid config. + function validConfig() internal pure returns (LibConfig.ConfigDetails memory cfg) { + cfg = LibConfig.ConfigDetails( + 10_000_000, // registryMaxGasCap + 5_000_000, // sysRegistryMaxGasCap + 0.001 ether, // automationBaseFeeWeiPerSec + 0.002 ether, // flatRegistrationFeeWei + 0.002 ether, // congestionBaseFeeWeiPerSec + 3600, // taskDurationCapSecs + 3600, // sysTaskDurationCapSecs + 2000, // cycleDurationSecs + 500, // taskCapacity + 500, // sysTaskCapacity + 55, // congestionThresholdPercentage + 3 // congestionExponent + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' updates the config buffer. + function testUpdateConfigBuffer() public { + LibConfig.ConfigDetails memory cfg = validConfig(); + + vm.prank(admin); + automationCore.updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + + // Pending config should be updated + LibConfig.ConfigDetails memory pendingCfg = automationCore.getPendingConfig(); + assertEq(pendingCfg.taskDurationCapSecs, cfg.taskDurationCapSecs); + assertEq(pendingCfg.registryMaxGasCap, cfg.registryMaxGasCap); + assertEq(pendingCfg.automationBaseFeeWeiPerSec, cfg.automationBaseFeeWeiPerSec); + assertEq(pendingCfg.flatRegistrationFeeWei, cfg.flatRegistrationFeeWei); + assertEq(pendingCfg.congestionThresholdPercentage, cfg.congestionThresholdPercentage); + assertEq(pendingCfg.congestionBaseFeeWeiPerSec, cfg.congestionBaseFeeWeiPerSec); + assertEq(pendingCfg.congestionExponent, cfg.congestionExponent); + assertEq(pendingCfg.taskCapacity, cfg.taskCapacity); + assertEq(pendingCfg.cycleDurationSecs, cfg.cycleDurationSecs); + assertEq(pendingCfg.sysTaskDurationCapSecs, cfg.sysTaskDurationCapSecs); + assertEq(pendingCfg.sysRegistryMaxGasCap, cfg.sysRegistryMaxGasCap); + assertEq(pendingCfg.sysTaskCapacity, cfg.sysTaskCapacity); + } + + /// @dev Test to ensure 'updateConfigBuffer' emits event 'ConfigBufferUpdated'. + function testUpdateConfigBufferEmitsEvent() public { + LibConfig.ConfigDetails memory cfg = validConfig(); + + vm.expectEmit(true, false, false, false); + emit AutomationCore.ConfigBufferUpdated(cfg); + + vm.prank(admin); + automationCore.updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + /// @dev Test to ensure 'updateConfigBuffer' reverts if caller is not owner. + function testUpdateConfigBufferRevertsIfNotOwner() public { + LibConfig.ConfigDetails memory cfg = validConfig(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + automationCore.updateConfigBuffer( + cfg.taskDurationCapSecs, + cfg.registryMaxGasCap, + cfg.automationBaseFeeWeiPerSec, + cfg.flatRegistrationFeeWei, + cfg.congestionThresholdPercentage, + cfg.congestionBaseFeeWeiPerSec, + cfg.congestionExponent, + cfg.taskCapacity, + cfg.cycleDurationSecs, + cfg.sysTaskDurationCapSecs, + cfg.sysRegistryMaxGasCap, + cfg.sysTaskCapacity + ); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'withdrawFees' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'withdrawFees' reverts if amount is zero. + function testWithdrawFeesRevertsIfAmountZero() public { + vm.prank(admin); + + vm.expectRevert(IAutomationCore.InvalidAmount.selector); + automationCore.withdrawFees(0, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if recipient address is zero. + function testWithdrawFeesRevertsIfRecipientAddressZero() public { + vm.prank(admin); + + vm.expectRevert(IAutomationCore.AddressCannotBeZero.selector); + automationCore.withdrawFees(1 ether, address(0)); + } + + /// @dev Test to ensure 'withdrawFees' reverts if contract has insufficient balance. + function testWithdrawFeesRevertsIfInsufficientBalance() public { + vm.expectRevert(IAutomationCore.InsufficientBalance.selector); + + vm.prank(admin); + automationCore.withdrawFees(1 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if request amount exceeds the locked balance. + function testWithdrawFeesRevertsIfRequestExceedsLockedBalance() public { + registerUST(); + + vm.expectRevert(IAutomationCore.RequestExceedsLockedBalance.selector); + + vm.prank(admin); + automationCore.withdrawFees(0.04 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' reverts if caller is not owner. + function testWithdrawFeesRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, alice)); + + vm.prank(alice); + automationCore.withdrawFees(1 ether, admin); + } + + /// @dev Test to ensure 'withdrawFees' withdraws the requested amount and updates the balance. + function testWithdrawFees() public { + registerUST(); + + assertEq(erc20Supra.balanceOf(admin), 0); + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.502 ether); + + vm.prank(admin); + automationCore.withdrawFees(0.002 ether, admin); + + assertEq(erc20Supra.balanceOf(admin), 0.002 ether); + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.5 ether); + } + + /// @dev Test to ensure 'withdrawFees' emits event 'RegistryFeeWithdrawn'. + function testWithdrawFeesEmitsEvent() public { + registerUST(); + + vm.expectEmit(true, true, false, false); + emit AutomationCore.RegistryFeeWithdrawn(admin, 0.002 ether); + + vm.prank(admin); + automationCore.withdrawFees(0.002 ether, admin); + } + + /// @dev Test to ensure 'applyPendingConfig' reverts if caller is not AutomationController. + function testApplyPendingConfigRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationCore.CallerNotController.selector); + + vm.prank(address(registry)); + automationCore.applyPendingConfig(); + } + + /// @dev Test to ensure 'safeUnlockLockedDeposit' reverts if caller is not AutomationController. + function testSafeUnlockLockedDepositRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationCore.CallerNotController.selector); + + vm.prank(address(registry)); + automationCore.safeUnlockLockedDeposit(0, 0.01 ether); + } + + /// @dev Test to ensure 'refundTaskFees' reverts if caller is not AutomationController. + function testRefundTaskFeesRevertsIfCallerNotAutomationController() public { + registerUST(); + CommonUtils.TaskDetails memory task = registry.getTaskDetails(0); + + vm.expectRevert(IAutomationCore.CallerNotController.selector); + + vm.prank(address(registry)); + automationCore.refundTaskFees( + uint64(block.timestamp), + uint64(block.timestamp) + 100000, + 0.0001 ether, + task + ); + } + + /// @dev Test to ensure 'updateStateForValidRegistration' reverts if caller is not AutomationRegistry. + function test_UpdateStateForValidRegistration_RevertsIfCallerNotAutomationRegistry() public { + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.CallerNotRegistry.selector); + + vm.prank(address(automationController)); + automationCore.updateStateForValidRegistration( + 10, + uint64(block.timestamp), + uint64(block.timestamp) + 2250, + CommonUtils.TaskType.UST, + payload, + 1000000, + 0.001 ether, + 0.01 ether + ); + } + + /// @dev Test to ensure 'incTotalDepositedAutomationFees' reverts if caller is not AutomationRegistry. + function testIncTotalDepositedAutomationFeesRevertsIfCallerNotAutomationRegistry() public { + vm.expectRevert(IAutomationCore.CallerNotRegistry.selector); + + vm.prank(address(automationController)); + automationCore.incTotalDepositedAutomationFees(0.01 ether); + } + + /// @dev Test to ensure 'refund' reverts if caller is not AutomationRegistry. + function testRefundRevertsIfCallerNotAutomationRegistry() public { + vm.expectRevert(IAutomationCore.CallerNotRegistry.selector); + + vm.prank(address(automationController)); + automationCore.refund(alice, 0.01 ether); + } + + /// @dev Test to ensure 'safeDepositRefund' reverts if caller is not AutomationRegistry. + function testSafeDepositRefundRevertsIfCallerNotAutomationRegistry() public { + vm.expectRevert(IAutomationCore.CallerNotRegistry.selector); + + vm.prank(address(automationController)); + automationCore.safeDepositRefund( + 0, + alice, + 0.01 ether, + 0.05 ether + ); + } + + /// @dev Test to ensure 'unlockDepositAndCycleFee' reverts if caller is not AutomationRegistry. + function testUnlockDepositAndCycleFeeRevertsIfCallerNotAutomationRegistry() public { + vm.expectRevert(IAutomationCore.CallerNotRegistry.selector); + + vm.prank(address(automationController)); + automationCore.unlockDepositAndCycleFee( + 0, + CommonUtils.TaskState.ACTIVE, + uint64(block.timestamp) + 2250, + 1000000, + 2000, + uint64(block.timestamp), + 0.01 ether + ); + } + + /// @dev Helper function to return payload. + /// @param _value Value to be sent along with the transaction. + /// @param _target Address of the destination smart contract. + function createPayload(uint128 _value, address _target) private pure returns (bytes memory) { + LibConfig.AccessListEntry[] memory accessList = new LibConfig.AccessListEntry[](2); + + bytes32[] memory keys = new bytes32[](2); + keys[0] = bytes32(uint256(0)); + keys[1] = bytes32(uint256(1)); + + accessList[0] = LibConfig.AccessListEntry({ + addr: address(0x1111), + storageKeys: keys + }); + + accessList[1] = LibConfig.AccessListEntry({ + addr: address(0x2222), + storageKeys: keys + }); + + bytes memory callData = abi.encodeCall(ERC20Supra.erc20SupraToNative, 100); + bytes memory payload = abi.encode(_value, _target, callData, accessList); + + return payload; + } + + /// @dev Helper function to register a UST. + function registerUST() private { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.startPrank(alice); + erc20Supra.nativeToErc20Supra{value: 5 ether}(); + erc20Supra.approve(address(automationCore), type(uint256).max); + + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 4, + auxData + ); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/solidity/supra_contracts/test/AutomationRegistry.t.sol b/solidity/supra_contracts/test/AutomationRegistry.t.sol new file mode 100644 index 0000000000..8eb4d6e2d6 --- /dev/null +++ b/solidity/supra_contracts/test/AutomationRegistry.t.sol @@ -0,0 +1,1161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; +import {AutomationRegistry} from "../src/AutomationRegistry.sol"; +import {AutomationCore} from "../src/AutomationCore.sol"; +import {AutomationController} from "../src/AutomationController.sol"; +import {IAutomationCore} from "../src/IAutomationCore.sol"; +import {IAutomationRegistry} from "../src/IAutomationRegistry.sol"; +import {ERC20Supra} from "../src/ERC20Supra.sol"; +import {LibConfig} from "../src/LibConfig.sol"; +import {LibRegistry} from "../src/LibRegistry.sol"; +import {CommonUtils} from "../src/CommonUtils.sol"; + +contract AutomationRegistryTest is Test { + ERC20Supra erc20Supra; // ERC20Supra contract + AutomationCore automationCore; // AutomationCore instance on proxy address + AutomationRegistry registry; // AutomationRegistry instance on proxy address + AutomationController controller; // AutomationController instance on proxy address + + /// @dev Address of the transaction hash precompile. + address constant TX_HASH_PRECOMPILE = 0x0000000000000000000000000000000053555001; + + address admin = address(0xA11CE); + address vmSigner = address(0x53555000); + address alice = address(0x123); + address bob = address(0x456); + + /// @dev Sets up initial state for testing. + /// @dev Sets balance of 'alice' to 100 ether. + /// @dev Deploys and initializes all contracts with required parameters. + function setUp() public { + vm.deal(alice, 100 ether); + + vm.startPrank(admin); + erc20Supra = new ERC20Supra(msg.sender); + + AutomationCore automationCoreImpl = new AutomationCore(); + bytes memory automationCoreInitData = abi.encodeCall( + AutomationCore.initialize, + ( + 3600, // taskDurationCapSecs + 10_000_000, // registryMaxGasCap + 0.001 ether, // automationBaseFeeWeiPerSec + 0.002 ether, // flatRegistrationFeeWei + 50, // congestionThresholdPercentage + 0.002 ether, // congestionBaseFeeWeiPerSec + 2, // congestionExponent + 500, // taskCapacity + 2000, // cycleDurationSecs + 3600, // sysTaskDurationCapSecs + 5_000_000, // sysRegistryMaxGasCap + 500, // sysTaskCapacity + vmSigner, // VM Signer address + address(erc20Supra) // ERC20Supra address + ) + ); + ERC1967Proxy automationCoreProxy = new ERC1967Proxy(address(automationCoreImpl), automationCoreInitData); + automationCore = AutomationCore(address(automationCoreProxy)); + + AutomationRegistry registryImpl = new AutomationRegistry(); + bytes memory registryInitData = abi.encodeCall(AutomationRegistry.initialize, (address(automationCore))); + ERC1967Proxy registryProxy = new ERC1967Proxy(address(registryImpl), registryInitData); + registry = AutomationRegistry(address(registryProxy)); + + AutomationController controllerImpl = new AutomationController(); + bytes memory controllerInitData = abi.encodeCall(AutomationController.initialize,(address(automationCore), address(registry), true)); + ERC1967Proxy controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + controller = AutomationController(address(controllerProxy)); + + automationCore.setAutomationRegistry(address(registry)); + automationCore.setAutomationController(address(controller)); + registry.setAutomationController(address(controller)); + + vm.stopPrank(); + + vm.mockCall( + TX_HASH_PRECOMPILE, + bytes(""), + abi.encode(keccak256("txHash")) + ); + } + + /// @dev Test to ensure all state variables are initialized correctly. + function testInitialize() public view { + assertEq(registry.owner(), admin); + assertEq(registry.automationCore(), address(automationCore)); + assertEq(registry.automationController(), address(controller)); + } + + /// @dev Test to ensure reinitialization fails. + function testInitializeRevertsIfReinitialized() public { + AutomationCore automationCoreImplementation = new AutomationCore(); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + + vm.prank(admin); + registry.initialize(address(automationCoreImplementation)); + } + + /// @dev Test to ensure initialization fails if AutomationCore address is zero. + function testInitializeRevertsIfAutomationCoreAddressIsZero() public { + AutomationRegistry implementation = new AutomationRegistry(); + bytes memory initData = abi.encodeCall(AutomationRegistry.initialize, (address(0))); + + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + new ERC1967Proxy(address(implementation), initData); + } + + /// @dev Test to ensure initialization fails if EOA is passed as AutomationCore address. + function testInitializeRevertsIfAutomationCoreAddressIsEoa() public { + AutomationRegistry implementation = new AutomationRegistry(); + bytes memory initData = abi.encodeCall(AutomationRegistry.initialize, (admin)); + + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + new ERC1967Proxy(address(implementation), initData); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'setAutomationController' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function that deploys AutomationController and returns its address. + function deployAutomationController() internal returns (address) { + // Deploy AutomationController proxy + AutomationController controllerImpl = new AutomationController(); + bytes memory controllerInitData = abi.encodeCall(AutomationController.initialize,(address(automationCore), address(registry), true)); + ERC1967Proxy controllerProxy = new ERC1967Proxy(address(controllerImpl), controllerInitData); + + return address(controllerProxy); + } + + /// @dev Test to ensure 'setAutomationController' updates the automation controller address. + function testSetAutomationController() public { + address controllerAddr = deployAutomationController(); + + vm.prank(admin); + registry.setAutomationController(controllerAddr); + + assertEq(registry.automationController(), controllerAddr); + } + + /// @dev Test to ensure 'setAutomationController' emits event 'AutomationControllerUpdated'. + function testSetAutomationControllerEmitsEvent() public { + address oldController = registry.automationController(); + address controllerAddr = deployAutomationController(); + + vm.expectEmit(true, true, false, false); + emit AutomationRegistry.AutomationControllerUpdated(oldController, controllerAddr); + + vm.prank(admin); + registry.setAutomationController(controllerAddr); + } + + /// @dev Test to ensure 'setAutomationController' reverts if caller is not owner. + function testSetAutomationControllerRevertsIfNotOwner() public { + address controllerAddr = deployAutomationController(); + + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + registry.setAutomationController(controllerAddr); + } + + /// @dev Test to ensure 'setAutomationController' reverts if zero address is passed. + function testSetAutomationControllerRevertsIfZeroAddress() public { + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + + vm.prank(admin); + registry.setAutomationController(address(0)); + } + + /// @dev Test to ensure 'setAutomationController' reverts if EOA is passed. + function testSetAutomationControllerRevertsIfEoa() public { + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(admin); + registry.setAutomationController(alice); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'grantAuthorization' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'grantAuthorization' grants authorization to an address. + function testGrantAuthorization() public { + vm.prank(admin); + registry.grantAuthorization(bob); + + assertTrue(registry.isAuthorizedSubmitter(bob)); + } + + /// @dev Test to ensure 'grantAuthorization' emits event 'AuthorizationGranted'. + function testGrantAuthorizationEmitsEvent() public { + vm.expectEmit(true, true, false, false); + emit AutomationRegistry.AuthorizationGranted(bob, block.timestamp); + + vm.prank(admin); + registry.grantAuthorization(bob); + } + + /// @dev Test to ensure 'grantAuthorization' reverts if address is already authorized. + function testGrantAuthorizationRevertsIfAlreadyAuthorised() public { + // Grant authorization to bob + testGrantAuthorization(); + + vm.expectRevert(IAutomationRegistry.AddressAlreadyExists.selector); + + vm.prank(admin); + registry.grantAuthorization(bob); + } + + /// @dev Test to ensure 'grantAuthorization' reverts if caller is not owner. + function testGrantAuthorizationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + registry.grantAuthorization(bob); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'revokeAuthorization' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'revokeAuthorization' revokes authorization from an address. + function testRevokeAuthorization() public { + // Grant authorization to bob + testGrantAuthorization(); + + // Revoke authorization + vm.prank(admin); + registry.revokeAuthorization(bob); + + assertFalse(registry.isAuthorizedSubmitter(bob)); + } + + /// @dev Test to ensure 'revokeAuthorization' emits event 'AuthorizationRevoked'. + function testRevokeAuthorizationEmitsEvent() public { + // Grant authorization to bob + testGrantAuthorization(); + + vm.expectEmit(true, true, false, false); + emit AutomationRegistry.AuthorizationRevoked(bob, block.timestamp); + + vm.prank(admin); + registry.revokeAuthorization(bob); + } + + /// @dev Test to ensure 'revokeAuthorization' reverts if address is not authorised. + function testRevokeAuthorizationRevertsIfNotAuthorised() public { + vm.expectRevert(IAutomationRegistry.AddressDoesNotExist.selector); + + vm.prank(admin); + registry.revokeAuthorization(bob); + } + + /// @dev Test to ensure 'revokeAuthorization' reverts if caller is not owner. + function testRevokeAuthorizationRevertsIfNotOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector,alice)); + + vm.prank(alice); + registry.revokeAuthorization(bob); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'register' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Helper function to return payload. + /// @param _value Value to be sent along with transaction. + /// @param _target Address of destination smart contract. + function createPayload(uint128 _value, address _target) private pure returns (bytes memory) { + LibConfig.AccessListEntry[] memory accessList = new LibConfig.AccessListEntry[](2); + + bytes32[] memory keys = new bytes32[](2); + keys[0] = bytes32(uint256(0)); + keys[1] = bytes32(uint256(1)); + + accessList[0] = LibConfig.AccessListEntry({ + addr: address(0x1111), + storageKeys: keys + }); + + accessList[1] = LibConfig.AccessListEntry({ + addr: address(0x2222), + storageKeys: keys + }); + + bytes memory callData = abi.encodeCall(ERC20Supra.erc20SupraToNative, 100); + bytes memory payload = abi.encode(_value, _target, callData, accessList); + + return payload; + } + + /// @dev Test to ensure 'register' reverts if automation is not enabled. + function testRegisterRevertsIfAutomationNotEnabled() public { + // Disable automation + vm.prank(admin); + controller.disableAutomation(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(alice); + registry.register( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + uint128(10 gwei), // gasPriceCap + uint128(0.5 ether), // automationFeeCapForCycle + 0, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'register' reverts if registration is disabled. + function testRegisterRevertsIfRegistrationDisabled() public { + // Disable registration + vm.prank(admin); + automationCore.disableRegistration(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.RegistrationDisabled.selector); + + vm.prank(alice); + registry.register( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + uint128(10 gwei), // gasPriceCap + uint128(0.5 ether), // automationFeeCapForCycle + 0, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'register' reverts if expiry time is equal to or less than registration time. + function testRegisterRevertsIfInvalidExpiryTime() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InvalidExpiryTime.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp), // Invalid expiryTime + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if task duration is greater than the task duration cap. + function testRegisterRevertsIfInvalidTaskDuration() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InvalidTaskDuration.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 3601), // Invalid task duration + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if task expires before the next cycle. + function testRegisterRevertsIfTaskExpiresBeforeNextCycle() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.TaskExpiresBeforeNextCycle.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2000), // Task expires before next cycle + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if payload target address is zero. + function testRegisterRevertsIfPayloadTargetZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(0)); // Invalid address: address(0) + + vm.expectRevert(CommonUtils.AddressCannotBeZero.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if payload target address is EOA. + function testRegisterRevertsIfPayloadTargetEoa() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, alice); // Invalid address: EOA address being passed + + vm.expectRevert(CommonUtils.AddressCannotBeEOA.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if 0 is passed as max gas amount. + function testRegisterRevertsIfMaxGasAmountZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InvalidMaxGasAmount.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(0), // maxGasAmount + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if 0 is passed as gas price cap. + function testRegisterRevertsIfGasPriceCapZero() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InvalidGasPriceCap.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(0), // gasPriceCap + uint128(0.5 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if automation fee cap is less than the estimated automation fee. + function testRegisterRevertsIfAutomationFeeCapLessThanEstimated() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InsufficientFeeCapForCycle.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0), // automationFeeCapForCycle + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' reverts if gas committed exceeds the registry max gas cap. + function testRegisterRevertsIfGasCommittedExceedsMaxGasCap() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.GasCommittedExceedsMaxGasCap.selector); + + vm.prank(alice); + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(10_000_001), // Gas exceeds max gas cap + uint128(10 gwei), + uint128(7.01 ether), + 0, + auxData + ); + } + + /// @dev Test to ensure 'register' registers a UST. + function testRegister() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.startPrank(alice); + erc20Supra.nativeToErc20Supra{value: 5 ether}(); + erc20Supra.approve(address(automationCore), type(uint256).max); + + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 4, + auxData + ); + vm.stopPrank(); + + CommonUtils.TaskDetails memory taskMetadata = registry.getTaskDetails(0); + assertTrue(registry.ifTaskExists(0)); + assertEq(registry.totalTasks(), 1); + assertEq(registry.getNextTaskIndex(), 1); + assertEq(automationCore.getGasCommittedForNextCycle(), 1_000_000); + assertEq(automationCore.getTotalDepositedAutomationFees(), 0.5 ether); + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.502 ether); + assertEq(erc20Supra.balanceOf(alice), 4.498 ether); + + assertEq(taskMetadata.maxGasAmount, 1_000_000); + assertEq(taskMetadata.gasPriceCap, 10 gwei); + assertEq(taskMetadata.automationFeeCapForCycle, 0.5 ether); + assertEq(taskMetadata.depositFee, 0.5 ether); + assertEq(taskMetadata.txHash, keccak256("txHash")); + assertEq(taskMetadata.taskIndex, 0); + assertEq(taskMetadata.registrationTime, uint64(block.timestamp)); + assertEq(taskMetadata.expiryTime, uint64(block.timestamp + 2250)); + assertEq(taskMetadata.priority, 0); + assertEq(uint8(taskMetadata.taskType), 0); + assertEq(uint8(taskMetadata.state), 0); + assertEq(taskMetadata.owner, alice); + assertEq(taskMetadata.payloadTx, payload); + assertEq(taskMetadata.auxData, auxData); + } + + /// @dev Test to ensure 'register' emits event 'TaskRegistered'. + function testRegisterEmitsEvent() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.startPrank(alice); + erc20Supra.nativeToErc20Supra{value: 5 ether}(); + erc20Supra.approve(address(automationCore), type(uint256).max); + + CommonUtils.TaskDetails memory taskMetadata = CommonUtils.TaskDetails( + 1_000_000, + 10 gwei, + 0.5 ether, + 0.5 ether, + keccak256("txHash"), + 0, + uint64(block.timestamp), + uint64(block.timestamp + 2250), + 0, + CommonUtils.TaskType.UST, + CommonUtils.TaskState.PENDING, + alice, + payload, + auxData + ); + + vm.expectEmit(true, true, false, true); + emit AutomationRegistry.TaskRegistered(0, alice, 0.002 ether, 0.5 ether, taskMetadata); + + registry.register( + payload, + uint64(block.timestamp + 2250), + uint128(1_000_000), + uint128(10 gwei), + uint128(0.5 ether), + 0, + auxData + ); + vm.stopPrank(); + } + + // ::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'registerSystemTask' ::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'registerSystemTask' reverts if caller is not authorized. + function testRegisterSystemTaskRevertsIfUnauthorizedCaller() public { + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationRegistry.UnauthorizedAccount.selector); + + vm.prank(alice); + registry.registerSystemTask( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if automation is not enabled. + function testRegisterSystemTaskRevertsIfAutomationNotEnabled() public { + testGrantAuthorization(); + + vm.prank(admin); + controller.disableAutomation(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(bob); + registry.registerSystemTask( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if registration is disabled. + function testRegisterSystemTaskRevertsIfRegistrationDisabled() public { + testGrantAuthorization(); + + vm.prank(admin); + automationCore.disableRegistration(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.RegistrationDisabled.selector); + + vm.prank(bob); + registry.registerSystemTask( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if task duration is greater than system task duration cap. + function testRegisterSystemTaskRevertsIfInvalidTaskDuration() public { + testGrantAuthorization(); + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.InvalidTaskDuration.selector); + + vm.prank(bob); + registry.registerSystemTask( + payload, + uint64(block.timestamp + 3601), // Invalid task duration + uint128(1_000_000), + 2, + auxData + ); + } + + /// @dev Test to ensure 'registerSystemTask' reverts if gas committed exceeds the system registry max gas cap. + function testRegisterSystemTaskRevertsIfGasCommittedExceedsMaxGasCap() public { + testGrantAuthorization(); + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.expectRevert(IAutomationCore.GasCommittedExceedsMaxGasCap.selector); + + vm.prank(bob); + registry.registerSystemTask( + payload, + uint64(block.timestamp + 2250), + uint128(5_000_001), // Gas exceeds max gas cap + 2, + auxData + ); + } + + /// @dev Test to ensure 'registerSystemTask' registers a GST. + function testRegisterSystemTask() public { + testGrantAuthorization(); + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + vm.prank(bob); + registry.registerSystemTask( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + + CommonUtils.TaskDetails memory taskMetadata = registry.getTaskDetails(0); + assertTrue(registry.ifTaskExists(0)); + assertTrue(registry.ifSysTaskExists(0)); + assertEq(registry.totalTasks(), 1); + assertEq(registry.totalSystemTasks(), 1); + assertEq(registry.getNextTaskIndex(), 1); + assertEq(automationCore.getSystemGasCommittedForNextCycle(), 1_000_000); + + assertEq(taskMetadata.maxGasAmount, 1_000_000); + assertEq(taskMetadata.gasPriceCap, 0); + assertEq(taskMetadata.automationFeeCapForCycle, 0); + assertEq(taskMetadata.depositFee, 0); + assertEq(taskMetadata.txHash, keccak256("txHash")); + assertEq(taskMetadata.taskIndex, 0); + assertEq(taskMetadata.registrationTime, uint64(block.timestamp)); + assertEq(taskMetadata.expiryTime, uint64(block.timestamp + 2250)); + assertEq(taskMetadata.priority, 2); + assertEq(uint8(taskMetadata.taskType), 1); + assertEq(uint8(taskMetadata.state), 0); + assertEq(taskMetadata.owner, bob); + assertEq(taskMetadata.payloadTx, payload); + assertEq(taskMetadata.auxData, auxData); + } + + /// @dev Test to ensure 'registerSystemTask' emits event 'SystemTaskRegistered'. + function testRegisterSystemTaskEmitsEvent() public { + testGrantAuthorization(); + + bytes[] memory auxData; + bytes memory payload = createPayload(0, address(erc20Supra)); + + CommonUtils.TaskDetails memory taskMetadata = CommonUtils.TaskDetails( + 1_000_000, + 0, + 0, + 0, + keccak256("txHash"), + 0, + uint64(block.timestamp), + uint64(block.timestamp + 2250), + 2, + CommonUtils.TaskType.GST, + CommonUtils.TaskState.PENDING, + bob, + payload, + auxData + ); + + vm.expectEmit(true, true, false, true); + emit AutomationRegistry.SystemTaskRegistered(0, bob, block.timestamp, taskMetadata); + + vm.prank(bob); + registry.registerSystemTask( + payload, // payload + uint64(block.timestamp + 2250), // expiryTime + uint128(1_000_000), // maxGasAmount + 2, // priority + auxData // aux data + ); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'cancelTask' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'cancelTask' reverts if automation is not enabled. + function testCancelTaskRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + controller.disableAutomation(); + + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(alice); + registry.cancelTask(0); + } + + /// @dev Test to ensure 'cancelTask' reverts if task does not exist. + function testCancelTaskRevertsIfTaskDoesNotExist() public { + vm.expectRevert(IAutomationRegistry.TaskDoesNotExist.selector); + + vm.prank(alice); + registry.cancelTask(0); + } + + /// @dev Test to ensure 'cancelTask' reverts if task type is not UST. + function testCancelTaskRevertsIfTaskTypeNotUST() public { + testRegisterSystemTask(); + vm.expectRevert(IAutomationRegistry.UnsupportedTaskOperation.selector); + + vm.prank(bob); + registry.cancelTask(0); + } + + /// @dev Test to ensure 'cancelTask' reverts if caller is not the task owner. + function testCancelTaskRevertsIfUnauthorizedCaller() public { + testRegister(); + vm.expectRevert(IAutomationRegistry.UnauthorizedAccount.selector); + + vm.prank(bob); + registry.cancelTask(0); + } + + /// @dev Test to ensure 'cancelTask' cancels a UST. + function testCancelTask() public { + testRegister(); + + vm.prank(alice); + registry.cancelTask(0); + + assertFalse(registry.ifTaskExists(0)); + assertEq(registry.totalTasks(), 0); + assertEq(automationCore.getGasCommittedForNextCycle(), 0); + assertEq(automationCore.getTotalDepositedAutomationFees(), 0); + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.252 ether); + assertEq(erc20Supra.balanceOf(alice), 4.748 ether); + } + + /// @dev Test to ensure 'cancelTask' emits event 'TaskCancelled'. + function testCancelTaskEmitsEvent() public { + testRegister(); + + vm.expectEmit(true, true, true, false); + emit AutomationRegistry.TaskCancelled(0, alice, keccak256("txHash")); + + vm.prank(alice); + registry.cancelTask(0); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'cancelSystemTask' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'cancelSystemTask' reverts if automation is not enabled. + function testCancelSystemTaskRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + controller.disableAutomation(); + + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(alice); + registry.cancelSystemTask(0); + } + + /// @dev Test to ensure 'cancelSystemTask' reverts if task does not exist. + function testCancelSystemTaskRevertsIfTaskDoesNotExist() public { + vm.expectRevert(IAutomationRegistry.TaskDoesNotExist.selector); + + vm.prank(alice); + registry.cancelSystemTask(0); + } + + /// @dev Test to ensure 'cancelSystemTask' reverts if task does not exist in system tasks. + function testCancelSystemTaskRevertsIfSystemTaskDoesNotExist() public { + testRegister(); + vm.expectRevert(IAutomationRegistry.SystemTaskDoesNotExist.selector); + + vm.prank(alice); + registry.cancelSystemTask(0); + } + + /// @dev Test to ensure 'cancelSystemTask' reverts if caller is not the task owner. + function testCancelSystemTaskRevertsIfUnauthorizedCaller() public { + testRegisterSystemTask(); + vm.expectRevert(IAutomationRegistry.UnauthorizedAccount.selector); + + vm.prank(alice); + registry.cancelSystemTask(0); + } + + /// @dev Test to ensure 'cancelSystemTask' cancels a GST. + function testCancelSystemTask() public { + testRegisterSystemTask(); + + vm.prank(bob); + registry.cancelSystemTask(0); + + assertFalse(registry.ifTaskExists(0)); + assertFalse(registry.ifSysTaskExists(0)); + assertEq(registry.totalTasks(), 0); + assertEq(registry.totalSystemTasks(), 0); + assertEq(automationCore.getSystemGasCommittedForNextCycle(), 0); + } + + /// @dev Test to ensure 'cancelSystemTask' emits event 'TaskCancelled'. + function testCancelSystemTaskEmitsEvent() public { + testRegisterSystemTask(); + + vm.expectEmit(true, true, true, false); + emit AutomationRegistry.TaskCancelled(0, bob, keccak256("txHash")); + + vm.prank(bob); + registry.cancelSystemTask(0); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'stopTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'stopTasks' reverts if automation is not enabled. + function testStopTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + controller.disableAutomation(); + + uint64[] memory taskIndexes; + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(alice); + registry.stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if input array is empty. + function testStopTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IAutomationRegistry.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + registry.stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if caller is not the task owner. + function testStopTasksRevertsIfUnauthorizedCaller() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IAutomationRegistry.UnauthorizedAccount.selector); + + vm.prank(bob); + registry.stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' reverts if task type is not UST. + function testStopTasksRevertsIfTaskTypeNotUST() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IAutomationRegistry.UnsupportedTaskOperation.selector); + + vm.prank(bob); + registry.stopTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopTasks' does nothing if task does not exist. + function testStopTasksDoesNothingIfTaskDoesNotExist() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + registry.stopTasks(taskIndexes); + + assertEq(registry.totalTasks(), 1); + assertEq(automationCore.getTotalDepositedAutomationFees(), 0.5 ether); + } + + /// @dev Test to ensure 'stopTasks' stops the input UST tasks. + function testStopTasks() public { + testRegister(); + address controllerAddr = registry.automationController(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.warp(2002); + vm.startPrank(vmSigner, vmSigner); + AutomationController(controllerAddr).monitorCycleEnd(); + AutomationController(controllerAddr).processTasks(2, taskIndexes); + vm.stopPrank(); + + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.702 ether); + assertEq(erc20Supra.balanceOf(alice), 4.298 ether); + + vm.prank(alice); + registry.stopTasks(taskIndexes); + + assertFalse(registry.ifTaskExists(0)); + assertEq(registry.totalTasks(), 0); + assertEq(automationCore.getGasCommittedForNextCycle(), 0); + assertEq(automationCore.getTotalDepositedAutomationFees(), 0); + assertEq(erc20Supra.balanceOf(address(automationCore)), 0.18955 ether); + assertEq(erc20Supra.balanceOf(alice), 4.81045 ether); + } + + /// @dev Test to ensure 'stopTasks' emits event 'TasksStopped'. + function testStopTasksEmitsEvent() public { + testRegister(); + address controllerAddr = registry.automationController(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.warp(2002); + vm.startPrank(vmSigner, vmSigner); + AutomationController(controllerAddr).monitorCycleEnd(); + AutomationController(controllerAddr).processTasks(2, taskIndexes); + vm.stopPrank(); + + LibRegistry.TaskStopped[] memory stoppedTasks = new LibRegistry.TaskStopped[](1); + stoppedTasks[0] = LibRegistry.TaskStopped(0, 0.5 ether, 0.01245 ether, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit AutomationRegistry.TasksStopped(stoppedTasks, alice); + + vm.prank(alice); + registry.stopTasks(taskIndexes); + } + + // :::::::::::::::::::::::::::::::::::::::::::::::::::::: Tests related to 'stopSystemTasks' :::::::::::::::::::::::::::::::::::::::::::::::::::::: + + /// @dev Test to ensure 'stopSystemTasks' reverts if automation is not enabled. + function testStopSystemTasksRevertsIfAutomationNotEnabled() public { + vm.prank(admin); + controller.disableAutomation(); + + uint64[] memory taskIndexes; + vm.expectRevert(IAutomationRegistry.AutomationNotEnabled.selector); + + vm.prank(alice); + registry.stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if input array is empty. + function testStopSystemTasksRevertsIfInputArrayEmpty() public { + uint64[] memory taskIndexes; + vm.expectRevert(IAutomationRegistry.TaskIndexesCannotBeEmpty.selector); + + vm.prank(alice); + registry.stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if caller is not the task owner. + function testStopSystemTasksRevertsIfUnauthorizedCaller() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IAutomationRegistry.UnauthorizedAccount.selector); + + vm.prank(alice); + registry.stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' reverts if task type is not GST. + function testStopSystemTasksRevertsIfTaskTypeNotGST() public { + testRegister(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.expectRevert(IAutomationRegistry.UnsupportedTaskOperation.selector); + + vm.prank(alice); + registry.stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'stopSystemTasks' does nothing if task does not exist. + function testStopSystemTasksDoesNothingIfTaskDoesNotExist() public { + testRegisterSystemTask(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 5; + + vm.prank(alice); + registry.stopSystemTasks(taskIndexes); + + assertEq(registry.totalTasks(), 1); + assertEq(registry.totalSystemTasks(), 1); + } + + /// @dev Test to ensure 'stopSystemTasks' stops the input GST tasks. + function testStopSystemTasks() public { + testRegisterSystemTask(); + address controllerAddr = registry.automationController(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.warp(2002); + vm.prank(vmSigner, vmSigner); + AutomationController(controllerAddr).monitorCycleEnd(); + + vm.prank(vmSigner); + AutomationController(controllerAddr).processTasks(2, taskIndexes); + + vm.prank(bob); + registry.stopSystemTasks(taskIndexes); + + assertFalse(registry.ifTaskExists(0)); + assertFalse(registry.ifSysTaskExists(0)); + assertEq(registry.totalTasks(), 0); + assertEq(registry.totalSystemTasks(), 0); + assertEq(automationCore.getSystemGasCommittedForNextCycle(), 1000000); + } + + /// @dev Test to ensure 'stopSystemTasks' emits event 'TasksStopped'. + function testStopSystemTasksEmitsEvent() public { + testRegisterSystemTask(); + address controllerAddr = registry.automationController(); + + uint64[] memory taskIndexes = new uint64[](1); + taskIndexes[0] = 0; + + vm.warp(2002); + vm.prank(vmSigner, vmSigner); + AutomationController(controllerAddr).monitorCycleEnd(); + + vm.prank(vmSigner); + AutomationController(controllerAddr).processTasks(2, taskIndexes); + + LibRegistry.TaskStopped[] memory stoppedTasks = new LibRegistry.TaskStopped[](1); + stoppedTasks[0] = LibRegistry.TaskStopped(0, 0, 0, keccak256("txHash")); + + vm.expectEmit(true, true, false, false); + emit AutomationRegistry.TasksStopped(stoppedTasks, bob); + + vm.prank(bob); + registry.stopSystemTasks(taskIndexes); + } + + /// @dev Test to ensure 'removeTask' reverts if caller is not AutomationController. + function testRemoveTaskRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationRegistry.CallerNotController.selector); + + vm.prank(address(automationCore)); + registry.removeTask(0, false); + } + + /// @dev Test to ensure 'updateTaskState' reverts if caller is not AutomationController. + function testUpdateTaskStateRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationRegistry.CallerNotController.selector); + + vm.prank(address(automationCore)); + registry.updateTaskState(0, CommonUtils.TaskState.ACTIVE); + } + + /// @dev Test to ensure 'updateTasks' reverts if caller is not AutomationController. + function testUpdateTasksRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationRegistry.CallerNotController.selector); + + vm.prank(address(automationCore)); + registry.updateTaskIds(CommonUtils.CycleState.STARTED); + } + + /// @dev Test to ensure 'refundDepositAndDrop' reverts if caller is not AutomationController. + function testRefundDepositAndDropRevertsIfCallerNotAutomationController() public { + vm.expectRevert(IAutomationRegistry.CallerNotController.selector); + + vm.prank(address(automationCore)); + registry.refundDepositAndDrop( + 0, + alice, + 0.01 ether, + 0.1 ether + ); + } +} \ No newline at end of file