From 3f0b934b80c606ec4a7fd53e17cdc077d9967c89 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 14:49:00 +0500 Subject: [PATCH 01/21] feat: slashing contracts --- examples/CRISP/client/src/App.tsx | 1 + .../client/src/components/Cards/Card.tsx | 25 +- .../client/src/components/Cards/PollCard.tsx | 10 +- .../client/src/components/CircularTiles.tsx | 18 +- .../CRISP/client/src/components/NavMenu.tsx | 25 +- .../NotificationAlert.context.tsx | 1 + .../voteManagement/VoteManagement.context.tsx | 1 + .../DailyPoll/components/ConfirmVote.tsx | 1 + .../src/pages/PollResult/PollResult.tsx | 2 + examples/CRISP/enclave.config.yaml | 13 +- .../packages/crisp-contracts/deploy/deploy.ts | 1 + .../crisp-contracts/deployed_contracts.json | 150 +++ examples/CRISP/server/.env.example | 2 +- packages/enclave-contracts/.gitignore | 13 + .../IBondingRegistry.json | 15 +- .../ICiphernodeRegistry.json | 179 +++- .../interfaces/IEnclave.sol/IEnclave.json | 56 +- .../ISlashingManager.json | 849 ++++++++++++++++ .../EnclaveTicketToken.json | 47 +- .../DkgPkVerifier.sol/DkgPkVerifier.json | 8 +- .../DkgPkVerifier.sol/ZKTranscriptLib.json | 2 +- .../contracts/E3RefundManager.sol | 71 +- .../enclave-contracts/contracts/Enclave.sol | 153 ++- .../contracts/interfaces/IBondingRegistry.sol | 9 +- .../interfaces/ICiphernodeRegistry.sol | 71 ++ .../contracts/interfaces/ICircuitVerifier.sol | 23 + .../contracts/interfaces/IE3RefundManager.sol | 6 +- .../contracts/interfaces/IEnclave.sol | 14 +- .../contracts/interfaces/ISlashVerifier.sol | 23 - .../contracts/interfaces/ISlashingManager.sol | 141 +-- .../contracts/registry/BondingRegistry.sol | 57 +- .../registry/CiphernodeRegistryOwnable.sol | 136 ++- .../contracts/slashing/SlashingManager.sol | 383 +++++-- .../contracts/test/MockCiphernodeRegistry.sol | 85 +- .../contracts/test/MockSlashingVerifier.sol | 23 +- .../contracts/token/EnclaveTicketToken.sol | 15 + .../contracts/token/EnclaveToken.sol | 3 +- .../enclave-contracts/deployed_contracts.json | 136 +++ .../ignition/modules/enclave.ts | 1 - .../ignition/modules/mockSlashingVerifier.ts | 6 +- .../ignition/modules/slashingManager.ts | 6 +- .../scripts/deployAndSave/enclave.ts | 1 - .../scripts/deployAndSave/slashingManager.ts | 15 +- .../scripts/deployEnclave.ts | 12 +- packages/enclave-contracts/scripts/utils.ts | 1 + .../test/E3Lifecycle/E3Integration.spec.ts | 10 +- .../enclave-contracts/test/Enclave.spec.ts | 10 +- .../test/Registry/BondingRegistry.spec.ts | 2 + .../CiphernodeRegistryOwnable.spec.ts | 3 +- .../test/Slashing/CommitteeExpulsion.spec.ts | 957 ++++++++++++++++++ .../test/Slashing/SlashingManager.spec.ts | 779 ++++++++++---- .../client/src/context/WizardContext.tsx | 4 + .../src/pages/steps/RequestComputation.tsx | 2 +- templates/default/enclave.config.yaml | 5 +- 54 files changed, 4014 insertions(+), 568 deletions(-) create mode 100644 packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json create mode 100644 packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol delete mode 100644 packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol create mode 100644 packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts diff --git a/examples/CRISP/client/src/App.tsx b/examples/CRISP/client/src/App.tsx index 335d3794d3..ad087a4442 100644 --- a/examples/CRISP/client/src/App.tsx +++ b/examples/CRISP/client/src/App.tsx @@ -45,6 +45,7 @@ const App: React.FC = () => { }) } })() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/examples/CRISP/client/src/components/Cards/Card.tsx b/examples/CRISP/client/src/components/Cards/Card.tsx index f55705491f..e5ded8c8fb 100644 --- a/examples/CRISP/client/src/components/Cards/Card.tsx +++ b/examples/CRISP/client/src/components/Cards/Card.tsx @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useEffect, useState } from 'react' +import React, { useMemo, useState } from 'react' interface CardProps { children: React.ReactNode @@ -17,22 +17,17 @@ interface CardProps { const Card: React.FC = ({ children, isActive, isDetails, checked, onChecked }) => { const [isClicked, setIsClicked] = useState(checked ?? false) - useEffect(() => { - setIsClicked(checked ?? false) - }, [checked]) + const derivedIsClicked = useMemo(() => { + if (isActive) return false + return checked ?? isClicked + }, [isActive, checked, isClicked]) const handleClick = () => { if (isDetails) return - if (onChecked) onChecked(!isClicked) - setIsClicked(!isClicked) + if (onChecked) onChecked(!derivedIsClicked) + setIsClicked(!derivedIsClicked) } - useEffect(() => { - if (isActive) { - setIsClicked(false) - } - }, [isActive]) - return (
= ({ children, isActive, isDetails, checked, onC ${!isDetails && 'shadow-md'} transform border-2 transition-all duration-300 ease-in-out - ${isClicked ? 'scale-105 border-lime-400' : ''} - ${isClicked ? 'border-lime-400' : 'border-slate-600/20'} - ${isClicked ? 'bg-white' : 'bg-slate-100'} + ${derivedIsClicked ? 'scale-105 border-lime-400' : ''} + ${derivedIsClicked ? 'border-lime-400' : 'border-slate-600/20'} + ${derivedIsClicked ? 'bg-white' : 'bg-slate-100'} ${!isDetails && 'hover:border-lime-300 hover:bg-white hover:shadow-lg'} flex w-full items-center justify-center `} diff --git a/examples/CRISP/client/src/components/Cards/PollCard.tsx b/examples/CRISP/client/src/components/Cards/PollCard.tsx index 25713063e0..235ebc0951 100644 --- a/examples/CRISP/client/src/components/Cards/PollCard.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCard.tsx @@ -4,9 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { PollOption, PollResult } from '@/model/poll.model' +import { PollResult } from '@/model/poll.model' import VotesBadge from '@/components/VotesBadge' import PollCardResult from '@/components/Cards/PollCardResult' import { formatDate, markWinner } from '@/utils/methods' @@ -15,7 +15,6 @@ import { usePublicClient } from 'wagmi' const PollCard: React.FC = ({ roundId, options, totalVotes, date, endTime }) => { const navigate = useNavigate() - const [results, setResults] = useState(options) const [isActive, setIsActive] = useState(true) const { roundState, setPollResult, currentRoundId } = useVoteManagementContext() const client = usePublicClient() @@ -40,10 +39,7 @@ const PollCard: React.FC = ({ roundId, options, totalVotes, date, en return () => clearInterval(interval) }, [endTime, client, isActive]) - useEffect(() => { - const newPollOptions = markWinner(options) - setResults(newPollOptions) - }, [options]) + const results = useMemo(() => markWinner(options), [options]) const handleNavigation = () => { if (isActive && isCurrentRound) { diff --git a/examples/CRISP/client/src/components/CircularTiles.tsx b/examples/CRISP/client/src/components/CircularTiles.tsx index 8ccf12c0a3..00c73d5f69 100644 --- a/examples/CRISP/client/src/components/CircularTiles.tsx +++ b/examples/CRISP/client/src/components/CircularTiles.tsx @@ -4,17 +4,23 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { memo } from 'react' +import { memo, useEffect, useState } from 'react' import CircularTile from './CircularTile' +const generateRotations = (count: number) => [...Array(count)].map(() => [0, 90, 180, 270][Math.floor(Math.random() * 4)]) + const CircularTiles = ({ count = 1, className }: { count?: number; className?: string }) => { + const [rotations, setRotations] = useState(() => generateRotations(count)) + + useEffect(() => { + setRotations(generateRotations(count)) + }, [count]) + return ( <> - {[...Array(count)].map((_i, index) => { - const rand_index = Math.floor(Math.random() * 4) - const rotation = [0, 90, 180, 270][rand_index] - return - })} + {rotations.map((rotation, index) => ( + + ))} ) } diff --git a/examples/CRISP/client/src/components/NavMenu.tsx b/examples/CRISP/client/src/components/NavMenu.tsx index 59034ebeb2..1f28b6062f 100644 --- a/examples/CRISP/client/src/components/NavMenu.tsx +++ b/examples/CRISP/client/src/components/NavMenu.tsx @@ -35,16 +35,19 @@ const NavMenu: React.FC = () => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) - const handleClickOutside = (event: MouseEvent) => { - if ( - isOpen && - menuRef.current && - !menuRef.current.contains(event.target as Node) && - !buttonRef.current?.contains(event.target as Node) - ) { - setIsOpen(false) - } - } + const handleClickOutside = React.useCallback( + (event: MouseEvent) => { + if ( + isOpen && + menuRef.current && + !menuRef.current.contains(event.target as Node) && + !buttonRef.current?.contains(event.target as Node) + ) { + setIsOpen(false) + } + }, + [isOpen], + ) const toggleMenu = (event: React.MouseEvent) => { event.stopPropagation() @@ -59,7 +62,7 @@ const NavMenu: React.FC = () => { return () => { document.removeEventListener('mousedown', handleClickOutside) } - }, [isOpen]) + }, [isOpen, handleClickOutside]) const handleNavigation = (path: string) => { navigate(path) diff --git a/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx b/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx index 34397220c3..159a390791 100644 --- a/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx +++ b/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx @@ -85,4 +85,5 @@ const NotificationAlertProvider = ({ children }: NotificationAlertProviderProps) ) } +// eslint-disable-next-line react-refresh/only-export-components export { useNotificationAlertContext, NotificationAlertProvider } diff --git a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx index 1299e8d105..ede1bea4ef 100644 --- a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx +++ b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx @@ -239,4 +239,5 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { ) } +// eslint-disable-next-line react-refresh/only-export-components export { useVoteManagementContext, VoteManagementProvider } diff --git a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx index 0393c73b38..a17d3200f5 100644 --- a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx +++ b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx @@ -15,6 +15,7 @@ const ConfirmVote: React.FC<{ confirmationUrl: string }> = ({ confirmationUrl }) return () => { setTxUrl(undefined) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx index 50107fa8cd..4c5db262f8 100644 --- a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx +++ b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx @@ -45,12 +45,14 @@ const PollResult: React.FC = () => { setLoading(false) } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pastPolls, roundId, roundState, activeTotalCount]) useEffect(() => { if (pollResult && loading) { setLoading(false) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pollResult]) return ( diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 976928cd43..71e18d12b6 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,20 +3,23 @@ chains: rpc_url: ws://localhost:8545 contracts: e3_program: - address: '0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB' + address: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8' deploy_block: 37 enclave: address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' - deploy_block: 15 + deploy_block: 13 ciphernode_registry: address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' - deploy_block: 13 + deploy_block: 11 bonding_registry: address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' - deploy_block: 10 + deploy_block: 8 fee_token: address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' - deploy_block: 5 + deploy_block: 4 + slashing_manager: + address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' + deploy_block: 8 program: dev: true nodes: diff --git a/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts b/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts index 90f7a359fd..5582d17bb0 100644 --- a/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts +++ b/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts @@ -16,6 +16,7 @@ const contractMapping: Record = { Enclave: 'enclave', CiphernodeRegistryOwnable: 'ciphernode_registry', BondingRegistry: 'bonding_registry', + SlashingManager: 'slashing_manager', MockUSDC: 'fee_token', } diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 43557cade4..98a62ff20d 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -154,5 +154,155 @@ "address": "0x6d97bDf6741905F63bd99e0EB920FFe5e5498544", "blockNumber": 10285602 } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001", + "ciphernodeRegistry": "0x0000000000000000000000000000000000000001", + "enclave": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "proxyRecords": { + "initData": "0x7333fa82000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000093a80", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "proxyAdminAddress": "0x94099942864EA81cCF197E9D71ac53310b1468D8", + "implementationAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "blockNumber": 8, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "10" + }, + "proxyRecords": { + "initData": "0x1794bb3c000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "proxyAdminAddress": "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + "implementationAddress": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "blockNumber": 11, + "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000" + ] + }, + "proxyRecords": { + "initData": "0x69c5b347000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", + "implementationAddress": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "blockNumber": 13, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockComputeProvider": { + "blockNumber": 29, + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + }, + "MockDecryptionVerifier": { + "blockNumber": 30, + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" + }, + "MockE3Program": { + "blockNumber": 31, + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + }, + "MockRISC0Verifier": { + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "blockNumber": 34 + }, + "HonkVerifier": { + "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", + "blockNumber": 36 + }, + "CRISPProgram": { + "address": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", + "blockNumber": 37, + "constructorArgs": { + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "verifierAddress": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "honkVerifierAddress": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", + "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" + } + }, + "MockVotingToken": { + "address": "0xf5059a5D33d5853360D16C683c16e67980206f36", + "blockNumber": 39 + } } } \ No newline at end of file diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 0d81df449f..58d0f1c6e4 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,7 +15,7 @@ CRON_API_KEY=1234567890 # Based on Default Hardhat Deployments (Only for testing) ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" CIPHERNODE_REGISTRY_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -E3_PROGRAM_ADDRESS="0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index 919a65a056..5d288546ee 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -3,20 +3,26 @@ .coverage_cache .coverage_contracts +# Base Dirs + # Base Dirs /artifacts/ !/artifacts/ /artifacts/** !/artifacts/contracts/ +# Interfaces + # Interfaces !/artifacts/contracts/interfaces/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ !/artifacts/contracts/interfaces/IBondingRegistry.sol/ +!/artifacts/contracts/interfaces/ISlashingManager.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json !/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +!/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json # Registry !/artifacts/contracts/registry/ @@ -26,6 +32,13 @@ !/artifacts/contracts/token/EnclaveTicketToken.sol/ !/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +# Verifier contracts +!/artifacts/contracts/verifier/ +!/artifacts/contracts/verifier/DkgPkVerifier.sol/ +!/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +!/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json + + # Verifier contracts !/artifacts/contracts/verifier/ !/artifacts/contracts/verifier/DkgPkVerifier.sol/ diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index a9daa05c33..b87aa0ec93 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -611,6 +611,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "distributor", + "type": "address" + } + ], + "name": "revokeRewardDistributor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -890,5 +903,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 7c6ad1ce87..28e91369d8 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -128,6 +128,37 @@ "name": "CommitteeFormationFailed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "activeCountAfter", + "type": "uint256" + } + ], + "name": "CommitteeMemberExpelled", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -190,6 +221,37 @@ "name": "CommitteeRequested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "activeCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thresholdM", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "viable", + "type": "bool" + } + ], + "name": "CommitteeViabilityUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -279,6 +341,40 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "expelCommitteeMember", + "outputs": [ + { + "internalType": "uint256", + "name": "activeCount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "thresholdM", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -298,6 +394,44 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getActiveCommitteeCount", + "outputs": [ + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getActiveCommitteeNodes", + "outputs": [ + { + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getBondingRegistry", @@ -349,6 +483,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getCommitteeThreshold", + "outputs": [ + { + "internalType": "uint32[2]", + "name": "threshold", + "type": "uint32[2]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -368,6 +521,30 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isCommitteeMemberActive", + "outputs": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -590,5 +767,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 28c6fdb611..2bfe082fbd 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -163,6 +163,19 @@ "name": "E3ProgramEnabled", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes[]", + "name": "e3ProgramParams", + "type": "bytes[]" + } + ], + "name": "E3ProgramsParamsRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -420,6 +433,19 @@ "name": "RewardsDistributed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "slashingManager", + "type": "address" + } + ], + "name": "SlashingManagerSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -439,11 +465,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "indexed": false, @@ -813,11 +834,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "internalType": "struct IEnclave.E3TimeoutConfig", @@ -954,6 +970,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "_e3ProgramsParams", + "type": "bytes[]" + } + ], + "name": "removeE3ProgramsParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1178,11 +1207,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "internalType": "struct IEnclave.E3TimeoutConfig", @@ -1202,5 +1226,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json new file mode 100644 index 0000000000..6ba870c913 --- /dev/null +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -0,0 +1,849 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "ISlashingManager", + "sourceName": "contracts/interfaces/ISlashingManager.sol", + "abi": [ + { + "inputs": [], + "name": "AlreadyAppealed", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyExecuted", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyResolved", + "type": "error" + }, + { + "inputs": [], + "name": "AppealPending", + "type": "error" + }, + { + "inputs": [], + "name": "AppealUpheld", + "type": "error" + }, + { + "inputs": [], + "name": "AppealWindowActive", + "type": "error" + }, + { + "inputs": [], + "name": "AppealWindowExpired", + "type": "error" + }, + { + "inputs": [], + "name": "CiphernodeBanned", + "type": "error" + }, + { + "inputs": [], + "name": "DuplicateEvidence", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPolicy", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProposal", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorNotInCommittee", + "type": "error" + }, + { + "inputs": [], + "name": "ProofIsValid", + "type": "error" + }, + { + "inputs": [], + "name": "ProofRequired", + "type": "error" + }, + { + "inputs": [], + "name": "SignerIsNotOperator", + "type": "error" + }, + { + "inputs": [], + "name": "SlashReasonDisabled", + "type": "error" + }, + { + "inputs": [], + "name": "SlashReasonNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "evidence", + "type": "string" + } + ], + "name": "AppealFiled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "indexed": false, + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "resolution", + "type": "string" + } + ], + "name": "AppealResolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "status", + "type": "bool" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "updater", + "type": "address" + } + ], + "name": "NodeBanUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "executed", + "type": "bool" + } + ], + "name": "SlashExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "name": "SlashPolicyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "executableAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "proposer", + "type": "address" + } + ], + "name": "SlashProposed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "slasher", + "type": "address" + } + ], + "name": "addSlasher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "bondingRegistry", + "outputs": [ + { + "internalType": "contract IBondingRegistry", + "name": "registry", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "executeSlash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "evidence", + "type": "string" + } + ], + "name": "fileAppeal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "getSlashPolicy", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "getSlashProposal", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "executed", + "type": "bool" + }, + { + "internalType": "bool", + "name": "appealed", + "type": "bool" + }, + { + "internalType": "bool", + "name": "resolved", + "type": "bool" + }, + { + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "proposedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executableAt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "proofHash", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "proofVerified", + "type": "bool" + } + ], + "internalType": "struct ISlashingManager.SlashProposal", + "name": "proposal", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isBanned", + "outputs": [ + { + "internalType": "bool", + "name": "isBanned", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "proposeSlash", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "evidence", + "type": "bytes" + } + ], + "name": "proposeSlashEvidence", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "slasher", + "type": "address" + } + ], + "name": "removeSlasher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "internalType": "string", + "name": "resolution", + "type": "string" + } + ], + "name": "resolveAppeal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newBondingRegistry", + "type": "address" + } + ], + "name": "setBondingRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "name": "setSlashPolicy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalProposals", + "outputs": [ + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "updateBanStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index c995f68ebf..17d318e4f7 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -508,12 +508,12 @@ "inputs": [ { "internalType": "address", - "name": "spender", + "name": "", "type": "address" }, { "internalType": "uint256", - "name": "value", + "name": "", "type": "uint256" } ], @@ -525,7 +525,7 @@ "type": "bool" } ], - "stateMutability": "nonpayable", + "stateMutability": "pure", "type": "function" }, { @@ -919,6 +919,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "payableBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1143,72 +1156,72 @@ "type": "function" } ], - "bytecode": "0x610180604052348015610010575f5ffd5b5060405161289638038061289683398101604081905261002f9161037d565b82816040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e00000000000000000000000081525080604051806040016040528060018152602001603160f81b8152506040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e0000000000000000000000008152506040518060400160405280600381526020016245544b60e81b81525081600390816100e3919061045f565b5060046100f0828261045f565b5061010091508390506005610226565b6101205261010f816006610226565b61014052815160208084019190912060e052815190820120610100524660a05261019b60e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b60805250503060c052506001600160a01b0381166101d357604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101dc81610258565b50306001600160a01b038216036102085760405163438d6fe360e01b81523060048201526024016101ca565b6001600160a01b03166101605261021e826102a9565b505050610571565b5f6020835110156102415761023a836102fa565b9050610252565b8161024c848261045f565b5060ff90505b92915050565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6102b1610337565b6001600160a01b0381166102d85760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b5f5f829050601f81511115610324578260405163305a27a960e01b81526004016101ca9190610519565b805161032f8261054e565b179392505050565b600b546001600160a01b031633146103645760405163118cdaa760e01b81523360048201526024016101ca565b565b6001600160a01b038116811461037a575f5ffd5b50565b5f5f5f6060848603121561038f575f5ffd5b835161039a81610366565b60208501519093506103ab81610366565b60408501519092506103bc81610366565b809150509250925092565b634e487b7160e01b5f52604160045260245ffd5b600181811c908216806103ef57607f821691505b60208210810361040d57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111561045a57805f5260205f20601f840160051c810160208510156104385750805b601f840160051c820191505b81811015610457575f8155600101610444565b50505b505050565b81516001600160401b03811115610478576104786103c7565b61048c8161048684546103db565b84610413565b6020601f8211600181146104be575f83156104a75750848201515b5f19600385901b1c1916600184901b178455610457565b5f84815260208120601f198516915b828110156104ed57878501518255602094850194600190920191016104cd565b508482101561050a57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b8051602080830151919081101561040d575f1960209190910360031b1b16919050565b60805160a05160c05160e051610100516101205161014051610160516122b56105e15f395f81816107c401528181610ba901528181610cda0152610d8701525f61119c01525f61116f01525f610ee701525f610ebf01525f610e1a01525f610e4401525f610e6e01526122b55ff3fe608060405234801561000f575f5ffd5b50600436106101c1575f3560e01c8063715018a6116100f657806395d89b411161009a57806395d89b41146103e05780639ab24eb0146103e8578063a9059cbb146103fb578063a91ee0dc1461040e578063c3cda52014610421578063d505accf1461042f578063dd62ed3e14610442578063f1127ed814610455578063f2fde38b14610494575f5ffd5b8063715018a6146103415780637b103999146103495780637ecebe001461035c57806384b0196e1461036f57806385bc898c1461038a5780638da5cb5b1461039d5780638e539e8c146103ae57806391ddadf4146103c1575f5ffd5b80633644e515116101685780633644e515146102805780633a46b1a8146102885780634bf5d7e91461029b578063587cde1e146102a35780635c19a95c146102c357806368a9674d146102d65780636f307dc3146102e95780636fcfff45146102f157806370a0823114610319575f5ffd5b806306fdde03146101c5578063095ea7b3146101e3578063117de2fd1461020657806318160ddd1461021b578063205c28781461022d57806323b872dd146102405780632f4f21e214610253578063313ce56714610266575b5f5ffd5b6101cd6104a7565b6040516101da9190611ec3565b60405180910390f35b6101f66101f1366004611eeb565b610537565b60405190151581526020016101da565b610219610214366004611eeb565b610550565b005b6002545b6040519081526020016101da565b6101f661023b366004611eeb565b610591565b6101f661024e366004611f13565b6105cf565b6101f6610261366004611eeb565b6105f2565b61026e61064d565b60405160ff90911681526020016101da565b61021f61065b565b61021f610296366004611eeb565b610664565b6101cd61069e565b6102b66102b1366004611f4d565b610716565b6040516101da9190611f66565b6102196102d1366004611f4d565b610733565b6101f66102e4366004611f13565b61074c565b6102b66107c2565b6103046102ff366004611f4d565b6107e6565b60405163ffffffff90911681526020016101da565b61021f610327366004611f4d565b6001600160a01b03165f9081526020819052604090205490565b6102196107f0565b600c546102b6906001600160a01b031681565b61021f61036a366004611f4d565b610803565b61037761080d565b6040516101da9796959493929190611f7a565b610219610398366004611eeb565b61084f565b600b546001600160a01b03166102b6565b61021f6103bc366004612010565b610884565b6103c96108a8565b60405165ffffffffffff90911681526020016101da565b6101cd6108b1565b61021f6103f6366004611f4d565b6108c0565b6101f6610409366004611eeb565b6108e0565b61021961041c366004611f4d565b6108ed565b6102196102d1366004612035565b61021961043d36600461208b565b61093e565b61021f6104503660046120f3565b610a79565b610468610463366004612124565b610aa3565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101da565b6102196104a2366004611f4d565b610ac0565b6060600380546104b690612161565b80601f01602080910402602001604051908101604052809291908181526020018280546104e290612161565b801561052d5780601f106105045761010080835404028352916020019161052d565b820191905f5260205f20905b81548152906001019060200180831161051057829003601f168201915b5050505050905090565b5f33610544818585610afd565b60019150505b92915050565b600c546001600160a01b0316331461057b57604051633217675b60e21b815260040160405180910390fd5b61058d6105866107c2565b8383610b0f565b5050565b600c545f906001600160a01b031633146105be57604051633217675b60e21b815260040160405180910390fd5b6105c88383610b6e565b9392505050565b5f336105dc858285610bd8565b6105e7858585610c29565b506001949350505050565b600c545f906001600160a01b0316331461061f57604051633217675b60e21b815260040160405180910390fd5b6106298383610c86565b90505f61063584610716565b6001600160a01b03160361054a5761054a8384610d0b565b5f610656610d84565b905090565b5f610656610e0e565b5f61068e61067183610f37565b6001600160a01b0385165f90815260096020526040902090610f85565b6001600160d01b03169392505050565b60606106a8611035565b65ffffffffffff166106b86108a8565b65ffffffffffff16146106de576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b0316331461077957604051633217675b60e21b815260040160405180910390fd5b61078c6107846107c2565b85308561103f565b6107968383611078565b5f6107a084610716565b6001600160a01b0316036107b8576107b88384610d0b565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61054a826110ac565b6107f86110cd565b6108015f6110fa565b565b5f61054a8261114b565b5f6060805f5f5f606061081e611168565b610826611195565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b0316331461087a57604051633217675b60e21b815260040160405180910390fd5b61058d82826111c2565b5f61089961089183610f37565b600a90610f85565b6001600160d01b031692915050565b5f610656611035565b6060600480546104b690612161565b6001600160a01b0381165f908152600960205260408120610899906111f6565b5f33610544818585610c29565b6108f56110cd565b6001600160a01b03811661091c5760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109675760405163313c898160e11b8152600481018590526024015b60405180910390fd5b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886109b28c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a0c8261122d565b90505f610a1b82878787611259565b9050896001600160a01b0316816001600160a01b031614610a62576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161095e565b610a6d8a8a8a610afd565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f80825260208201526105c88383611285565b610ac86110cd565b6001600160a01b038116610af1575f604051631e4fbdf760e01b815260040161095e9190611f66565b610afa816110fa565b50565b610b0a83838360016112b9565b505050565b6040516001600160a01b03838116602483015260448201839052610b0a91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061138b565b5f306001600160a01b03841603610b9a578260405163ec442f0560e01b815260040161095e9190611f66565b610ba433836111c2565b610bcf7f00000000000000000000000000000000000000000000000000000000000000008484610b0f565b50600192915050565b5f610be38484610a79565b90505f19811015610c235781811015610c1557828183604051637dc7a0d960e11b815260040161095e93929190612199565b610c2384848484035f6112b9565b50505050565b6001600160a01b038316610c52575f604051634b637e8f60e11b815260040161095e9190611f66565b6001600160a01b038216610c7b575f60405163ec442f0560e01b815260040161095e9190611f66565b610b0a8383836113ee565b5f33308103610caa5730604051634b637e8f60e11b815260040161095e9190611f66565b306001600160a01b03851603610cd5578360405163ec442f0560e01b815260040161095e9190611f66565b610d017f000000000000000000000000000000000000000000000000000000000000000082308661103f565b6105448484611078565b5f610d1583610716565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610b0a8183610d7f86611437565b611454565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610dff575060408051601f3d908101601f19168201909252610dfc918101906121ba565b60015b610e095750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610e6657507f000000000000000000000000000000000000000000000000000000000000000046145b15610e9057507f000000000000000000000000000000000000000000000000000000000000000090565b610656604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610f416108a8565b90508065ffffffffffff168310610f7c57604051637669fc0f60e11b81526004810184905265ffffffffffff8216602482015260440161095e565b6105c8836115bd565b81545f9081816005811115610fe1575f610f9e846115f3565b610fa890856121e9565b5f8881526020902090915081015465ffffffffffff9081169087161015610fd157809150610fdf565b610fdc8160016121fc565b92505b505b5f610fee87878585611746565b9050801561102857611012876110056001846121e9565b5f91825260209091200190565b54600160301b90046001600160d01b031661102a565b5f5b979650505050505050565b5f610656436115bd565b6040516001600160a01b038481166024830152838116604483015260648201839052610c239186918216906323b872dd90608401610b3c565b6001600160a01b0382166110a1575f60405163ec442f0560e01b815260040161095e9190611f66565b61058d5f83836113ee565b6001600160a01b0381165f9081526009602052604081205461054a906117a5565b600b546001600160a01b03163314610801573360405163118cdaa760e01b815260040161095e9190611f66565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f9081526007602052604081205461054a565b60606106567f000000000000000000000000000000000000000000000000000000000000000060056117d5565b60606106567f000000000000000000000000000000000000000000000000000000000000000060066117d5565b6001600160a01b0382166111eb575f604051634b637e8f60e11b815260040161095e9190611f66565b61058d825f836113ee565b80545f9080156112255761120f836110056001846121e9565b54600160301b90046001600160d01b03166105c8565b5f9392505050565b5f61054a611239610e0e565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112698888888861187e565b925092509250611279828261193c565b50909695505050505050565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206105c890836119f4565b6001600160a01b0384166112e2575f60405163e602df0560e01b815260040161095e9190611f66565b6001600160a01b03831661130b575f604051634a1406b160e11b815260040161095e9190611f66565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610c2357826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161137d91815260200190565b60405180910390a350505050565b5f5f60205f8451602086015f885af1806113aa576040513d5f823e3d81fd5b50505f513d915081156113c15780600114156113ce565b6001600160a01b0384163b155b15610c235783604051635274afe760e01b815260040161095e9190611f66565b6001600160a01b0383161580159061140e57506001600160a01b03821615155b1561142c57604051638cd22d1960e01b815260040160405180910390fd5b610b0a838383611a61565b6001600160a01b0381165f9081526020819052604081205461054a565b816001600160a01b0316836001600160a01b03161415801561147557505f81115b15610b0a576001600160a01b0383161561151c576001600160a01b0383165f90815260096020526040812081906114b790611ac76114b286611ad2565b611b05565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611511929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610b0a576001600160a01b0382165f908152600960205260408120819061155490611b3d6114b286611ad2565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115ae929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff8211156115ef576040516306dfcc6560e41b8152603060048201526024810183905260440161095e565b5090565b5f60018211611600575090565b816001600160801b82106116195760809190911c9060401b5b600160401b821061162f5760409190911c9060201b5b64010000000082106116465760209190911c9060101b5b62010000821061165b5760109190911c9060081b5b610100821061166f5760089190911c9060041b5b601082106116825760049190911c9060021b5b6004821061168e5760011b5b600302600190811c908185816116a6576116a661220f565b048201901c905060018185816116be576116be61220f565b048201901c905060018185816116d6576116d661220f565b048201901c905060018185816116ee576116ee61220f565b048201901c905060018185816117065761170661220f565b048201901c9050600181858161171e5761171e61220f565b048201901c905061173d8185816117375761173761220f565b04821190565b90039392505050565b5f5b8183101561179d575f61175b8484611b48565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff16111561178957809250611797565b6117948160016121fc565b93505b50611748565b509392505050565b5f63ffffffff8211156115ef576040516306dfcc6560e41b8152602060048201526024810183905260440161095e565b606060ff83146117ef576117e883611b62565b905061054a565b8180546117fb90612161565b80601f016020809104026020016040519081016040528092919081815260200182805461182790612161565b80156118725780601f1061184957610100808354040283529160200191611872565b820191905f5260205f20905b81548152906001019060200180831161185557829003601f168201915b5050505050905061054a565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411156118ad57505f91506003905082611932565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156118fe573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811661192957505f925060019150829050611932565b92505f91508190505b9450945094915050565b5f82600381111561194f5761194f612223565b03611958575050565b600182600381111561196c5761196c612223565b0361198a5760405163f645eedf60e01b815260040160405180910390fd5b600282600381111561199e5761199e612223565b036119bf5760405163fce698f760e01b81526004810182905260240161095e565b60038260038111156119d3576119d3612223565b0361058d576040516335e2f38360e21b81526004810182905260240161095e565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611a2157611a21612237565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611a6c838383611b9f565b6001600160a01b038316611abc575f611a8460025490565b90506001600160d01b0380821115611ab957604051630e58ae9360e11b8152600481018390526024810182905260440161095e565b50505b610b0a838383611cb2565b5f6105c8828461224b565b5f6001600160d01b038211156115ef576040516306dfcc6560e41b815260d060048201526024810183905260440161095e565b5f5f611b30611b126108a8565b611b28611b1e886111f6565b868863ffffffff16565b879190611d11565b915091505b935093915050565b5f6105c8828461226a565b5f611b566002848418612289565b6105c8908484166121fc565b60605f611b6e83611d1e565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611bc9578060025f828254611bbe91906121fc565b90915550611c269050565b6001600160a01b0383165f9081526020819052604090205481811015611c085783818360405163391434e360e21b815260040161095e93929190612199565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611c4257600280548290039055611c60565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611ca591815260200190565b60405180910390a3505050565b6001600160a01b038316611cd457611cd1600a611b3d6114b284611ad2565b50505b6001600160a01b038216611cf657611cf3600a611ac76114b284611ad2565b50505b610b0a611d0284610716565b611d0b84610716565b83611454565b5f80611b30858585611d45565b5f60ff8216601f81111561054a57604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611e3b575f611d61876110056001856121e9565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611da457604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611ddd57825465ffffffffffff16600160301b6001600160d01b03891602178355611e2d565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611b3592505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611b35565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6105c86020830184611e95565b80356001600160a01b0381168114610e09575f5ffd5b5f5f60408385031215611efc575f5ffd5b611f0583611ed5565b946020939093013593505050565b5f5f5f60608486031215611f25575f5ffd5b611f2e84611ed5565b9250611f3c60208501611ed5565b929592945050506040919091013590565b5f60208284031215611f5d575f5ffd5b6105c882611ed5565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f611f9860e0830189611e95565b8281036040840152611faa8189611e95565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015611fff578351835260209384019390920191600101611fe1565b50909b9a5050505050505050505050565b5f60208284031215612020575f5ffd5b5035919050565b60ff81168114610afa575f5ffd5b5f5f5f5f5f5f60c0878903121561204a575f5ffd5b61205387611ed5565b95506020870135945060408701359350606087013561207181612027565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a0312156120a1575f5ffd5b6120aa88611ed5565b96506120b860208901611ed5565b9550604088013594506060880135935060808801356120d681612027565b9699959850939692959460a0840135945060c09093013592915050565b5f5f60408385031215612104575f5ffd5b61210d83611ed5565b915061211b60208401611ed5565b90509250929050565b5f5f60408385031215612135575f5ffd5b61213e83611ed5565b9150602083013563ffffffff81168114612156575f5ffd5b809150509250929050565b600181811c9082168061217557607f821691505b60208210810361219357634e487b7160e01b5f52602260045260245ffd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b5f602082840312156121ca575f5ffd5b81516105c881612027565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561054a5761054a6121d5565b8082018082111561054a5761054a6121d5565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561054a5761054a6121d5565b6001600160d01b03818116838216019081111561054a5761054a6121d5565b5f826122a357634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106101c1575f3560e01c8063715018a6116100f657806395d89b411161009a57806395d89b41146103e05780639ab24eb0146103e8578063a9059cbb146103fb578063a91ee0dc1461040e578063c3cda52014610421578063d505accf1461042f578063dd62ed3e14610442578063f1127ed814610455578063f2fde38b14610494575f5ffd5b8063715018a6146103415780637b103999146103495780637ecebe001461035c57806384b0196e1461036f57806385bc898c1461038a5780638da5cb5b1461039d5780638e539e8c146103ae57806391ddadf4146103c1575f5ffd5b80633644e515116101685780633644e515146102805780633a46b1a8146102885780634bf5d7e91461029b578063587cde1e146102a35780635c19a95c146102c357806368a9674d146102d65780636f307dc3146102e95780636fcfff45146102f157806370a0823114610319575f5ffd5b806306fdde03146101c5578063095ea7b3146101e3578063117de2fd1461020657806318160ddd1461021b578063205c28781461022d57806323b872dd146102405780632f4f21e214610253578063313ce56714610266575b5f5ffd5b6101cd6104a7565b6040516101da9190611ec3565b60405180910390f35b6101f66101f1366004611eeb565b610537565b60405190151581526020016101da565b610219610214366004611eeb565b610550565b005b6002545b6040519081526020016101da565b6101f661023b366004611eeb565b610591565b6101f661024e366004611f13565b6105cf565b6101f6610261366004611eeb565b6105f2565b61026e61064d565b60405160ff90911681526020016101da565b61021f61065b565b61021f610296366004611eeb565b610664565b6101cd61069e565b6102b66102b1366004611f4d565b610716565b6040516101da9190611f66565b6102196102d1366004611f4d565b610733565b6101f66102e4366004611f13565b61074c565b6102b66107c2565b6103046102ff366004611f4d565b6107e6565b60405163ffffffff90911681526020016101da565b61021f610327366004611f4d565b6001600160a01b03165f9081526020819052604090205490565b6102196107f0565b600c546102b6906001600160a01b031681565b61021f61036a366004611f4d565b610803565b61037761080d565b6040516101da9796959493929190611f7a565b610219610398366004611eeb565b61084f565b600b546001600160a01b03166102b6565b61021f6103bc366004612010565b610884565b6103c96108a8565b60405165ffffffffffff90911681526020016101da565b6101cd6108b1565b61021f6103f6366004611f4d565b6108c0565b6101f6610409366004611eeb565b6108e0565b61021961041c366004611f4d565b6108ed565b6102196102d1366004612035565b61021961043d36600461208b565b61093e565b61021f6104503660046120f3565b610a79565b610468610463366004612124565b610aa3565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101da565b6102196104a2366004611f4d565b610ac0565b6060600380546104b690612161565b80601f01602080910402602001604051908101604052809291908181526020018280546104e290612161565b801561052d5780601f106105045761010080835404028352916020019161052d565b820191905f5260205f20905b81548152906001019060200180831161051057829003601f168201915b5050505050905090565b5f33610544818585610afd565b60019150505b92915050565b600c546001600160a01b0316331461057b57604051633217675b60e21b815260040160405180910390fd5b61058d6105866107c2565b8383610b0f565b5050565b600c545f906001600160a01b031633146105be57604051633217675b60e21b815260040160405180910390fd5b6105c88383610b6e565b9392505050565b5f336105dc858285610bd8565b6105e7858585610c29565b506001949350505050565b600c545f906001600160a01b0316331461061f57604051633217675b60e21b815260040160405180910390fd5b6106298383610c86565b90505f61063584610716565b6001600160a01b03160361054a5761054a8384610d0b565b5f610656610d84565b905090565b5f610656610e0e565b5f61068e61067183610f37565b6001600160a01b0385165f90815260096020526040902090610f85565b6001600160d01b03169392505050565b60606106a8611035565b65ffffffffffff166106b86108a8565b65ffffffffffff16146106de576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b0316331461077957604051633217675b60e21b815260040160405180910390fd5b61078c6107846107c2565b85308561103f565b6107968383611078565b5f6107a084610716565b6001600160a01b0316036107b8576107b88384610d0b565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61054a826110ac565b6107f86110cd565b6108015f6110fa565b565b5f61054a8261114b565b5f6060805f5f5f606061081e611168565b610826611195565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b0316331461087a57604051633217675b60e21b815260040160405180910390fd5b61058d82826111c2565b5f61089961089183610f37565b600a90610f85565b6001600160d01b031692915050565b5f610656611035565b6060600480546104b690612161565b6001600160a01b0381165f908152600960205260408120610899906111f6565b5f33610544818585610c29565b6108f56110cd565b6001600160a01b03811661091c5760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109675760405163313c898160e11b8152600481018590526024015b60405180910390fd5b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886109b28c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a0c8261122d565b90505f610a1b82878787611259565b9050896001600160a01b0316816001600160a01b031614610a62576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161095e565b610a6d8a8a8a610afd565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f80825260208201526105c88383611285565b610ac86110cd565b6001600160a01b038116610af1575f604051631e4fbdf760e01b815260040161095e9190611f66565b610afa816110fa565b50565b610b0a83838360016112b9565b505050565b6040516001600160a01b03838116602483015260448201839052610b0a91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061138b565b5f306001600160a01b03841603610b9a578260405163ec442f0560e01b815260040161095e9190611f66565b610ba433836111c2565b610bcf7f00000000000000000000000000000000000000000000000000000000000000008484610b0f565b50600192915050565b5f610be38484610a79565b90505f19811015610c235781811015610c1557828183604051637dc7a0d960e11b815260040161095e93929190612199565b610c2384848484035f6112b9565b50505050565b6001600160a01b038316610c52575f604051634b637e8f60e11b815260040161095e9190611f66565b6001600160a01b038216610c7b575f60405163ec442f0560e01b815260040161095e9190611f66565b610b0a8383836113ee565b5f33308103610caa5730604051634b637e8f60e11b815260040161095e9190611f66565b306001600160a01b03851603610cd5578360405163ec442f0560e01b815260040161095e9190611f66565b610d017f000000000000000000000000000000000000000000000000000000000000000082308661103f565b6105448484611078565b5f610d1583610716565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610b0a8183610d7f86611437565b611454565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610dff575060408051601f3d908101601f19168201909252610dfc918101906121ba565b60015b610e095750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610e6657507f000000000000000000000000000000000000000000000000000000000000000046145b15610e9057507f000000000000000000000000000000000000000000000000000000000000000090565b610656604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610f416108a8565b90508065ffffffffffff168310610f7c57604051637669fc0f60e11b81526004810184905265ffffffffffff8216602482015260440161095e565b6105c8836115bd565b81545f9081816005811115610fe1575f610f9e846115f3565b610fa890856121e9565b5f8881526020902090915081015465ffffffffffff9081169087161015610fd157809150610fdf565b610fdc8160016121fc565b92505b505b5f610fee87878585611746565b9050801561102857611012876110056001846121e9565b5f91825260209091200190565b54600160301b90046001600160d01b031661102a565b5f5b979650505050505050565b5f610656436115bd565b6040516001600160a01b038481166024830152838116604483015260648201839052610c239186918216906323b872dd90608401610b3c565b6001600160a01b0382166110a1575f60405163ec442f0560e01b815260040161095e9190611f66565b61058d5f83836113ee565b6001600160a01b0381165f9081526009602052604081205461054a906117a5565b600b546001600160a01b03163314610801573360405163118cdaa760e01b815260040161095e9190611f66565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f9081526007602052604081205461054a565b60606106567f000000000000000000000000000000000000000000000000000000000000000060056117d5565b60606106567f000000000000000000000000000000000000000000000000000000000000000060066117d5565b6001600160a01b0382166111eb575f604051634b637e8f60e11b815260040161095e9190611f66565b61058d825f836113ee565b80545f9080156112255761120f836110056001846121e9565b54600160301b90046001600160d01b03166105c8565b5f9392505050565b5f61054a611239610e0e565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112698888888861187e565b925092509250611279828261193c565b50909695505050505050565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206105c890836119f4565b6001600160a01b0384166112e2575f60405163e602df0560e01b815260040161095e9190611f66565b6001600160a01b03831661130b575f604051634a1406b160e11b815260040161095e9190611f66565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610c2357826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161137d91815260200190565b60405180910390a350505050565b5f5f60205f8451602086015f885af1806113aa576040513d5f823e3d81fd5b50505f513d915081156113c15780600114156113ce565b6001600160a01b0384163b155b15610c235783604051635274afe760e01b815260040161095e9190611f66565b6001600160a01b0383161580159061140e57506001600160a01b03821615155b1561142c57604051638cd22d1960e01b815260040160405180910390fd5b610b0a838383611a61565b6001600160a01b0381165f9081526020819052604081205461054a565b816001600160a01b0316836001600160a01b03161415801561147557505f81115b15610b0a576001600160a01b0383161561151c576001600160a01b0383165f90815260096020526040812081906114b790611ac76114b286611ad2565b611b05565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611511929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610b0a576001600160a01b0382165f908152600960205260408120819061155490611b3d6114b286611ad2565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115ae929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff8211156115ef576040516306dfcc6560e41b8152603060048201526024810183905260440161095e565b5090565b5f60018211611600575090565b816001600160801b82106116195760809190911c9060401b5b600160401b821061162f5760409190911c9060201b5b64010000000082106116465760209190911c9060101b5b62010000821061165b5760109190911c9060081b5b610100821061166f5760089190911c9060041b5b601082106116825760049190911c9060021b5b6004821061168e5760011b5b600302600190811c908185816116a6576116a661220f565b048201901c905060018185816116be576116be61220f565b048201901c905060018185816116d6576116d661220f565b048201901c905060018185816116ee576116ee61220f565b048201901c905060018185816117065761170661220f565b048201901c9050600181858161171e5761171e61220f565b048201901c905061173d8185816117375761173761220f565b04821190565b90039392505050565b5f5b8183101561179d575f61175b8484611b48565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff16111561178957809250611797565b6117948160016121fc565b93505b50611748565b509392505050565b5f63ffffffff8211156115ef576040516306dfcc6560e41b8152602060048201526024810183905260440161095e565b606060ff83146117ef576117e883611b62565b905061054a565b8180546117fb90612161565b80601f016020809104026020016040519081016040528092919081815260200182805461182790612161565b80156118725780601f1061184957610100808354040283529160200191611872565b820191905f5260205f20905b81548152906001019060200180831161185557829003601f168201915b5050505050905061054a565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411156118ad57505f91506003905082611932565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156118fe573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811661192957505f925060019150829050611932565b92505f91508190505b9450945094915050565b5f82600381111561194f5761194f612223565b03611958575050565b600182600381111561196c5761196c612223565b0361198a5760405163f645eedf60e01b815260040160405180910390fd5b600282600381111561199e5761199e612223565b036119bf5760405163fce698f760e01b81526004810182905260240161095e565b60038260038111156119d3576119d3612223565b0361058d576040516335e2f38360e21b81526004810182905260240161095e565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611a2157611a21612237565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611a6c838383611b9f565b6001600160a01b038316611abc575f611a8460025490565b90506001600160d01b0380821115611ab957604051630e58ae9360e11b8152600481018390526024810182905260440161095e565b50505b610b0a838383611cb2565b5f6105c8828461224b565b5f6001600160d01b038211156115ef576040516306dfcc6560e41b815260d060048201526024810183905260440161095e565b5f5f611b30611b126108a8565b611b28611b1e886111f6565b868863ffffffff16565b879190611d11565b915091505b935093915050565b5f6105c8828461226a565b5f611b566002848418612289565b6105c8908484166121fc565b60605f611b6e83611d1e565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611bc9578060025f828254611bbe91906121fc565b90915550611c269050565b6001600160a01b0383165f9081526020819052604090205481811015611c085783818360405163391434e360e21b815260040161095e93929190612199565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611c4257600280548290039055611c60565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611ca591815260200190565b60405180910390a3505050565b6001600160a01b038316611cd457611cd1600a611b3d6114b284611ad2565b50505b6001600160a01b038216611cf657611cf3600a611ac76114b284611ad2565b50505b610b0a611d0284610716565b611d0b84610716565b83611454565b5f80611b30858585611d45565b5f60ff8216601f81111561054a57604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611e3b575f611d61876110056001856121e9565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611da457604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611ddd57825465ffffffffffff16600160301b6001600160d01b03891602178355611e2d565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611b3592505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611b35565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6105c86020830184611e95565b80356001600160a01b0381168114610e09575f5ffd5b5f5f60408385031215611efc575f5ffd5b611f0583611ed5565b946020939093013593505050565b5f5f5f60608486031215611f25575f5ffd5b611f2e84611ed5565b9250611f3c60208501611ed5565b929592945050506040919091013590565b5f60208284031215611f5d575f5ffd5b6105c882611ed5565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f611f9860e0830189611e95565b8281036040840152611faa8189611e95565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015611fff578351835260209384019390920191600101611fe1565b50909b9a5050505050505050505050565b5f60208284031215612020575f5ffd5b5035919050565b60ff81168114610afa575f5ffd5b5f5f5f5f5f5f60c0878903121561204a575f5ffd5b61205387611ed5565b95506020870135945060408701359350606087013561207181612027565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a0312156120a1575f5ffd5b6120aa88611ed5565b96506120b860208901611ed5565b9550604088013594506060880135935060808801356120d681612027565b9699959850939692959460a0840135945060c09093013592915050565b5f5f60408385031215612104575f5ffd5b61210d83611ed5565b915061211b60208401611ed5565b90509250929050565b5f5f60408385031215612135575f5ffd5b61213e83611ed5565b9150602083013563ffffffff81168114612156575f5ffd5b809150509250929050565b600181811c9082168061217557607f821691505b60208210810361219357634e487b7160e01b5f52602260045260245ffd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b5f602082840312156121ca575f5ffd5b81516105c881612027565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561054a5761054a6121d5565b8082018082111561054a5761054a6121d5565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561054a5761054a6121d5565b6001600160d01b03818116838216019081111561054a5761054a6121d5565b5f826122a357634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", + "bytecode": "0x610180604052348015610010575f5ffd5b5060405161292e38038061292e83398101604081905261002f9161037d565b82816040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e00000000000000000000000081525080604051806040016040528060018152602001603160f81b8152506040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e0000000000000000000000008152506040518060400160405280600381526020016245544b60e81b81525081600390816100e3919061045f565b5060046100f0828261045f565b5061010091508390506005610226565b6101205261010f816006610226565b61014052815160208084019190912060e052815190820120610100524660a05261019b60e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b60805250503060c052506001600160a01b0381166101d357604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101dc81610258565b50306001600160a01b038216036102085760405163438d6fe360e01b81523060048201526024016101ca565b6001600160a01b03166101605261021e826102a9565b505050610571565b5f6020835110156102415761023a836102fa565b9050610252565b8161024c848261045f565b5060ff90505b92915050565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6102b1610337565b6001600160a01b0381166102d85760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b5f5f829050601f81511115610324578260405163305a27a960e01b81526004016101ca9190610519565b805161032f8261054e565b179392505050565b600b546001600160a01b031633146103645760405163118cdaa760e01b81523360048201526024016101ca565b565b6001600160a01b038116811461037a575f5ffd5b50565b5f5f5f6060848603121561038f575f5ffd5b835161039a81610366565b60208501519093506103ab81610366565b60408501519092506103bc81610366565b809150509250925092565b634e487b7160e01b5f52604160045260245ffd5b600181811c908216806103ef57607f821691505b60208210810361040d57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111561045a57805f5260205f20601f840160051c810160208510156104385750805b601f840160051c820191505b81811015610457575f8155600101610444565b50505b505050565b81516001600160401b03811115610478576104786103c7565b61048c8161048684546103db565b84610413565b6020601f8211600181146104be575f83156104a75750848201515b5f19600385901b1c1916600184901b178455610457565b5f84815260208120601f198516915b828110156104ed57878501518255602094850194600190920191016104cd565b508482101561050a57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b8051602080830151919081101561040d575f1960209190910360031b1b16919050565b60805160a05160c05160e0516101005161012051610140516101605161234d6105e15f395f818161084301528181610c2d01528181610d5e0152610e0b01525f61122701525f6111fa01525f610f6b01525f610f4301525f610e9e01525f610ec801525f610ef2015261234d5ff3fe608060405234801561000f575f5ffd5b50600436106101cc575f3560e01c806370a082311161010157806395d89b411161009a57806395d89b41146103f45780639ab24eb0146103fc578063a9059cbb1461040f578063a91ee0dc14610422578063c3cda52014610435578063d505accf14610443578063dd62ed3e14610456578063f1127ed814610469578063f2fde38b146104a8575f5ffd5b806370a082311461032d578063715018a6146103555780637b1039991461035d5780637ecebe001461037057806384b0196e1461038357806385bc898c1461039e5780638da5cb5b146103b15780638e539e8c146103c257806391ddadf4146103d5575f5ffd5b80633644e515116101735780633644e5151461028b5780633a46b1a81461029357806344b279a2146102a65780634bf5d7e9146102af578063587cde1e146102b75780635c19a95c146102d757806368a9674d146102ea5780636f307dc3146102fd5780636fcfff4514610305575f5ffd5b806306fdde03146101d0578063095ea7b3146101ee578063117de2fd1461021157806318160ddd14610226578063205c28781461023857806323b872dd1461024b5780632f4f21e21461025e578063313ce56714610271575b5f5ffd5b6101d86104bb565b6040516101e59190611f5b565b60405180910390f35b6102016101fc366004611f83565b61054b565b60405190151581526020016101e5565b61022461021f366004611f83565b610565565b005b6002545b6040519081526020016101e5565b610201610246366004611f83565b61060e565b610201610259366004611fab565b61064e565b61020161026c366004611f83565b610671565b6102796106cc565b60405160ff90911681526020016101e5565b61022a6106da565b61022a6102a1366004611f83565b6106e3565b61022a600d5481565b6101d861071d565b6102ca6102c5366004611fe5565b610795565b6040516101e59190611ffe565b6102246102e5366004611fe5565b6107b2565b6102016102f8366004611fab565b6107cb565b6102ca610841565b610318610313366004611fe5565b610865565b60405163ffffffff90911681526020016101e5565b61022a61033b366004611fe5565b6001600160a01b03165f9081526020819052604090205490565b61022461086f565b600c546102ca906001600160a01b031681565b61022a61037e366004611fe5565b610882565b61038b61088c565b6040516101e59796959493929190612012565b6102246103ac366004611f83565b6108ce565b600b546001600160a01b03166102ca565b61022a6103d03660046120a8565b61091a565b6103dd61093e565b60405165ffffffffffff90911681526020016101e5565b6101d8610947565b61022a61040a366004611fe5565b610956565b61020161041d366004611f83565b610976565b610224610430366004611fe5565b610983565b6102246102e53660046120cd565b610224610451366004612123565b6109d4565b61022a61046436600461218b565b610b0a565b61047c6104773660046121bc565b610b34565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101e5565b6102246104b6366004611fe5565b610b51565b6060600380546104ca906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546104f6906121f9565b80156105415780601f1061051857610100808354040283529160200191610541565b820191905f5260205f20905b81548152906001019060200180831161052457829003601f168201915b5050505050905090565b5f604051638cd22d1960e01b815260040160405180910390fd5b600c546001600160a01b0316331461059057604051633217675b60e21b815260040160405180910390fd5b600d548111156105e15760405162461bcd60e51b8152602060048201526017602482015276457863656564732070617961626c652062616c616e636560481b60448201526064015b60405180910390fd5b80600d5f8282546105f29190612245565b9091555061060a9050610603610841565b8383610b8e565b5050565b600c545f906001600160a01b0316331461063b57604051633217675b60e21b815260040160405180910390fd5b6106458383610bf2565b90505b92915050565b5f3361065b858285610c5c565b610666858585610cad565b506001949350505050565b600c545f906001600160a01b0316331461069e57604051633217675b60e21b815260040160405180910390fd5b6106a88383610d0a565b90505f6106b484610795565b6001600160a01b031603610648576106488384610d8f565b5f6106d5610e08565b905090565b5f6106d5610e92565b5f61070d6106f083610fbb565b6001600160a01b0385165f90815260096020526040902090611010565b6001600160d01b03169392505050565b60606107276110c0565b65ffffffffffff1661073761093e565b65ffffffffffff161461075d576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b031633146107f857604051633217675b60e21b815260040160405180910390fd5b61080b610803610841565b8530856110ca565b6108158383611103565b5f61081f84610795565b6001600160a01b031603610837576108378384610d8f565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61064882611137565b610877611158565b6108805f611185565b565b5f610648826111d6565b5f6060805f5f5f606061089d6111f3565b6108a5611220565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b031633146108f957604051633217675b60e21b815260040160405180910390fd5b80600d5f82825461090a9190612258565b9091555061060a9050828261124d565b5f61092f61092783610fbb565b600a90611010565b6001600160d01b031692915050565b5f6106d56110c0565b6060600480546104ca906121f9565b6001600160a01b0381165f90815260096020526040812061092f90611281565b5f33610837818585610cad565b61098b611158565b6001600160a01b0381166109b25760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109f85760405163313c898160e11b8152600481018590526024016105d8565b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610a438c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a9d826112b8565b90505f610aac828787876112e4565b9050896001600160a01b0316816001600160a01b031614610af3576040516325c0072360e11b81526001600160a01b0380831660048301528b1660248201526044016105d8565b610afe8a8a8a611310565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f8082526020820152610645838361131d565b610b59611158565b6001600160a01b038116610b82575f604051631e4fbdf760e01b81526004016105d89190611ffe565b610b8b81611185565b50565b6040516001600160a01b03838116602483015260448201839052610bed91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050611351565b505050565b5f306001600160a01b03841603610c1e578260405163ec442f0560e01b81526004016105d89190611ffe565b610c28338361124d565b610c537f00000000000000000000000000000000000000000000000000000000000000008484610b8e565b50600192915050565b5f610c678484610b0a565b90505f19811015610ca75781811015610c9957828183604051637dc7a0d960e11b81526004016105d89392919061226b565b610ca784848484035f6113b4565b50505050565b6001600160a01b038316610cd6575f604051634b637e8f60e11b81526004016105d89190611ffe565b6001600160a01b038216610cff575f60405163ec442f0560e01b81526004016105d89190611ffe565b610bed838383611486565b5f33308103610d2e5730604051634b637e8f60e11b81526004016105d89190611ffe565b306001600160a01b03851603610d59578360405163ec442f0560e01b81526004016105d89190611ffe565b610d857f00000000000000000000000000000000000000000000000000000000000000008230866110ca565b6108378484611103565b5f610d9983610795565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610bed8183610e03866114cf565b6114ec565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610e83575060408051601f3d908101601f19168201909252610e809181019061228c565b60015b610e8d5750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610eea57507f000000000000000000000000000000000000000000000000000000000000000046145b15610f1457507f000000000000000000000000000000000000000000000000000000000000000090565b6106d5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610fc561093e565b90508065ffffffffffff16831061100057604051637669fc0f60e11b81526004810184905265ffffffffffff821660248201526044016105d8565b61100983611655565b9392505050565b81545f908181600581111561106c575f6110298461168b565b6110339085612245565b5f8881526020902090915081015465ffffffffffff908116908716101561105c5780915061106a565b611067816001612258565b92505b505b5f611079878785856117de565b905080156110b35761109d87611090600184612245565b5f91825260209091200190565b54600160301b90046001600160d01b03166110b5565b5f5b979650505050505050565b5f6106d543611655565b6040516001600160a01b038481166024830152838116604483015260648201839052610ca79186918216906323b872dd90608401610bbb565b6001600160a01b03821661112c575f60405163ec442f0560e01b81526004016105d89190611ffe565b61060a5f8383611486565b6001600160a01b0381165f908152600960205260408120546106489061183d565b600b546001600160a01b03163314610880573360405163118cdaa760e01b81526004016105d89190611ffe565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f90815260076020526040812054610648565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600561186d565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600661186d565b6001600160a01b038216611276575f604051634b637e8f60e11b81526004016105d89190611ffe565b61060a825f83611486565b80545f9080156112b05761129a83611090600184612245565b54600160301b90046001600160d01b0316611009565b5f9392505050565b5f6106486112c4610e92565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112f488888888611916565b92509250925061130482826119d4565b50909695505050505050565b610bed83838360016113b4565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206106459083611a8c565b5f5f60205f8451602086015f885af180611370576040513d5f823e3d81fd5b50505f513d91508115611387578060011415611394565b6001600160a01b0384163b155b15610ca75783604051635274afe760e01b81526004016105d89190611ffe565b6001600160a01b0384166113dd575f60405163e602df0560e01b81526004016105d89190611ffe565b6001600160a01b038316611406575f604051634a1406b160e11b81526004016105d89190611ffe565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610ca757826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161147891815260200190565b60405180910390a350505050565b6001600160a01b038316158015906114a657506001600160a01b03821615155b156114c457604051638cd22d1960e01b815260040160405180910390fd5b610bed838383611af9565b6001600160a01b0381165f90815260208190526040812054610648565b816001600160a01b0316836001600160a01b03161415801561150d57505f81115b15610bed576001600160a01b038316156115b4576001600160a01b0383165f908152600960205260408120819061154f90611b5f61154a86611b6a565b611b9d565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115a9929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610bed576001600160a01b0382165f90815260096020526040812081906115ec90611bd561154a86611b6a565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611646929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff821115611687576040516306dfcc6560e41b815260306004820152602481018390526044016105d8565b5090565b5f60018211611698575090565b816001600160801b82106116b15760809190911c9060401b5b600160401b82106116c75760409190911c9060201b5b64010000000082106116de5760209190911c9060101b5b6201000082106116f35760109190911c9060081b5b61010082106117075760089190911c9060041b5b6010821061171a5760049190911c9060021b5b600482106117265760011b5b600302600190811c9081858161173e5761173e6122a7565b048201901c90506001818581611756576117566122a7565b048201901c9050600181858161176e5761176e6122a7565b048201901c90506001818581611786576117866122a7565b048201901c9050600181858161179e5761179e6122a7565b048201901c905060018185816117b6576117b66122a7565b048201901c90506117d58185816117cf576117cf6122a7565b04821190565b90039392505050565b5f5b81831015611835575f6117f38484611be0565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff1611156118215780925061182f565b61182c816001612258565b93505b506117e0565b509392505050565b5f63ffffffff821115611687576040516306dfcc6560e41b815260206004820152602481018390526044016105d8565b606060ff83146118875761188083611bfa565b9050610648565b818054611893906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546118bf906121f9565b801561190a5780601f106118e15761010080835404028352916020019161190a565b820191905f5260205f20905b8154815290600101906020018083116118ed57829003601f168201915b50505050509050610648565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561194557505f915060039050826119ca565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015611996573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b0381166119c157505f9250600191508290506119ca565b92505f91508190505b9450945094915050565b5f8260038111156119e7576119e76122bb565b036119f0575050565b6001826003811115611a0457611a046122bb565b03611a225760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115611a3657611a366122bb565b03611a575760405163fce698f760e01b8152600481018290526024016105d8565b6003826003811115611a6b57611a6b6122bb565b0361060a576040516335e2f38360e21b8152600481018290526024016105d8565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611ab957611ab96122cf565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611b04838383611c37565b6001600160a01b038316611b54575f611b1c60025490565b90506001600160d01b0380821115611b5157604051630e58ae9360e11b815260048101839052602481018290526044016105d8565b50505b610bed838383611d4a565b5f61064582846122e3565b5f6001600160d01b03821115611687576040516306dfcc6560e41b815260d06004820152602481018390526044016105d8565b5f5f611bc8611baa61093e565b611bc0611bb688611281565b868863ffffffff16565b879190611da9565b915091505b935093915050565b5f6106458284612302565b5f611bee6002848418612321565b61064590848416612258565b60605f611c0683611db6565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611c61578060025f828254611c569190612258565b90915550611cbe9050565b6001600160a01b0383165f9081526020819052604090205481811015611ca05783818360405163391434e360e21b81526004016105d89392919061226b565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611cda57600280548290039055611cf8565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611d3d91815260200190565b60405180910390a3505050565b6001600160a01b038316611d6c57611d69600a611bd561154a84611b6a565b50505b6001600160a01b038216611d8e57611d8b600a611b5f61154a84611b6a565b50505b610bed611d9a84610795565b611da384610795565b836114ec565b5f80611bc8858585611ddd565b5f60ff8216601f81111561064857604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611ed3575f611df987611090600185612245565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611e3c57604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611e7557825465ffffffffffff16600160301b6001600160d01b03891602178355611ec5565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611bcd92505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611bcd565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6106456020830184611f2d565b80356001600160a01b0381168114610e8d575f5ffd5b5f5f60408385031215611f94575f5ffd5b611f9d83611f6d565b946020939093013593505050565b5f5f5f60608486031215611fbd575f5ffd5b611fc684611f6d565b9250611fd460208501611f6d565b929592945050506040919091013590565b5f60208284031215611ff5575f5ffd5b61064582611f6d565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f61203060e0830189611f2d565b82810360408401526120428189611f2d565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015612097578351835260209384019390920191600101612079565b50909b9a5050505050505050505050565b5f602082840312156120b8575f5ffd5b5035919050565b60ff81168114610b8b575f5ffd5b5f5f5f5f5f5f60c087890312156120e2575f5ffd5b6120eb87611f6d565b955060208701359450604087013593506060870135612109816120bf565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a031215612139575f5ffd5b61214288611f6d565b965061215060208901611f6d565b95506040880135945060608801359350608088013561216e816120bf565b9699959850939692959460a0840135945060c09093013592915050565b5f5f6040838503121561219c575f5ffd5b6121a583611f6d565b91506121b360208401611f6d565b90509250929050565b5f5f604083850312156121cd575f5ffd5b6121d683611f6d565b9150602083013563ffffffff811681146121ee575f5ffd5b809150509250929050565b600181811c9082168061220d57607f821691505b60208210810361222b57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561064857610648612231565b8082018082111561064857610648612231565b6001600160a01b039390931683526020830191909152604082015260600190565b5f6020828403121561229c575f5ffd5b8151611009816120bf565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561064857610648612231565b6001600160d01b03818116838216019081111561064857610648612231565b5f8261233b57634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106101cc575f3560e01c806370a082311161010157806395d89b411161009a57806395d89b41146103f45780639ab24eb0146103fc578063a9059cbb1461040f578063a91ee0dc14610422578063c3cda52014610435578063d505accf14610443578063dd62ed3e14610456578063f1127ed814610469578063f2fde38b146104a8575f5ffd5b806370a082311461032d578063715018a6146103555780637b1039991461035d5780637ecebe001461037057806384b0196e1461038357806385bc898c1461039e5780638da5cb5b146103b15780638e539e8c146103c257806391ddadf4146103d5575f5ffd5b80633644e515116101735780633644e5151461028b5780633a46b1a81461029357806344b279a2146102a65780634bf5d7e9146102af578063587cde1e146102b75780635c19a95c146102d757806368a9674d146102ea5780636f307dc3146102fd5780636fcfff4514610305575f5ffd5b806306fdde03146101d0578063095ea7b3146101ee578063117de2fd1461021157806318160ddd14610226578063205c28781461023857806323b872dd1461024b5780632f4f21e21461025e578063313ce56714610271575b5f5ffd5b6101d86104bb565b6040516101e59190611f5b565b60405180910390f35b6102016101fc366004611f83565b61054b565b60405190151581526020016101e5565b61022461021f366004611f83565b610565565b005b6002545b6040519081526020016101e5565b610201610246366004611f83565b61060e565b610201610259366004611fab565b61064e565b61020161026c366004611f83565b610671565b6102796106cc565b60405160ff90911681526020016101e5565b61022a6106da565b61022a6102a1366004611f83565b6106e3565b61022a600d5481565b6101d861071d565b6102ca6102c5366004611fe5565b610795565b6040516101e59190611ffe565b6102246102e5366004611fe5565b6107b2565b6102016102f8366004611fab565b6107cb565b6102ca610841565b610318610313366004611fe5565b610865565b60405163ffffffff90911681526020016101e5565b61022a61033b366004611fe5565b6001600160a01b03165f9081526020819052604090205490565b61022461086f565b600c546102ca906001600160a01b031681565b61022a61037e366004611fe5565b610882565b61038b61088c565b6040516101e59796959493929190612012565b6102246103ac366004611f83565b6108ce565b600b546001600160a01b03166102ca565b61022a6103d03660046120a8565b61091a565b6103dd61093e565b60405165ffffffffffff90911681526020016101e5565b6101d8610947565b61022a61040a366004611fe5565b610956565b61020161041d366004611f83565b610976565b610224610430366004611fe5565b610983565b6102246102e53660046120cd565b610224610451366004612123565b6109d4565b61022a61046436600461218b565b610b0a565b61047c6104773660046121bc565b610b34565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101e5565b6102246104b6366004611fe5565b610b51565b6060600380546104ca906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546104f6906121f9565b80156105415780601f1061051857610100808354040283529160200191610541565b820191905f5260205f20905b81548152906001019060200180831161052457829003601f168201915b5050505050905090565b5f604051638cd22d1960e01b815260040160405180910390fd5b600c546001600160a01b0316331461059057604051633217675b60e21b815260040160405180910390fd5b600d548111156105e15760405162461bcd60e51b8152602060048201526017602482015276457863656564732070617961626c652062616c616e636560481b60448201526064015b60405180910390fd5b80600d5f8282546105f29190612245565b9091555061060a9050610603610841565b8383610b8e565b5050565b600c545f906001600160a01b0316331461063b57604051633217675b60e21b815260040160405180910390fd5b6106458383610bf2565b90505b92915050565b5f3361065b858285610c5c565b610666858585610cad565b506001949350505050565b600c545f906001600160a01b0316331461069e57604051633217675b60e21b815260040160405180910390fd5b6106a88383610d0a565b90505f6106b484610795565b6001600160a01b031603610648576106488384610d8f565b5f6106d5610e08565b905090565b5f6106d5610e92565b5f61070d6106f083610fbb565b6001600160a01b0385165f90815260096020526040902090611010565b6001600160d01b03169392505050565b60606107276110c0565b65ffffffffffff1661073761093e565b65ffffffffffff161461075d576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b031633146107f857604051633217675b60e21b815260040160405180910390fd5b61080b610803610841565b8530856110ca565b6108158383611103565b5f61081f84610795565b6001600160a01b031603610837576108378384610d8f565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61064882611137565b610877611158565b6108805f611185565b565b5f610648826111d6565b5f6060805f5f5f606061089d6111f3565b6108a5611220565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b031633146108f957604051633217675b60e21b815260040160405180910390fd5b80600d5f82825461090a9190612258565b9091555061060a9050828261124d565b5f61092f61092783610fbb565b600a90611010565b6001600160d01b031692915050565b5f6106d56110c0565b6060600480546104ca906121f9565b6001600160a01b0381165f90815260096020526040812061092f90611281565b5f33610837818585610cad565b61098b611158565b6001600160a01b0381166109b25760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109f85760405163313c898160e11b8152600481018590526024016105d8565b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610a438c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a9d826112b8565b90505f610aac828787876112e4565b9050896001600160a01b0316816001600160a01b031614610af3576040516325c0072360e11b81526001600160a01b0380831660048301528b1660248201526044016105d8565b610afe8a8a8a611310565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f8082526020820152610645838361131d565b610b59611158565b6001600160a01b038116610b82575f604051631e4fbdf760e01b81526004016105d89190611ffe565b610b8b81611185565b50565b6040516001600160a01b03838116602483015260448201839052610bed91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050611351565b505050565b5f306001600160a01b03841603610c1e578260405163ec442f0560e01b81526004016105d89190611ffe565b610c28338361124d565b610c537f00000000000000000000000000000000000000000000000000000000000000008484610b8e565b50600192915050565b5f610c678484610b0a565b90505f19811015610ca75781811015610c9957828183604051637dc7a0d960e11b81526004016105d89392919061226b565b610ca784848484035f6113b4565b50505050565b6001600160a01b038316610cd6575f604051634b637e8f60e11b81526004016105d89190611ffe565b6001600160a01b038216610cff575f60405163ec442f0560e01b81526004016105d89190611ffe565b610bed838383611486565b5f33308103610d2e5730604051634b637e8f60e11b81526004016105d89190611ffe565b306001600160a01b03851603610d59578360405163ec442f0560e01b81526004016105d89190611ffe565b610d857f00000000000000000000000000000000000000000000000000000000000000008230866110ca565b6108378484611103565b5f610d9983610795565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610bed8183610e03866114cf565b6114ec565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610e83575060408051601f3d908101601f19168201909252610e809181019061228c565b60015b610e8d5750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610eea57507f000000000000000000000000000000000000000000000000000000000000000046145b15610f1457507f000000000000000000000000000000000000000000000000000000000000000090565b6106d5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610fc561093e565b90508065ffffffffffff16831061100057604051637669fc0f60e11b81526004810184905265ffffffffffff821660248201526044016105d8565b61100983611655565b9392505050565b81545f908181600581111561106c575f6110298461168b565b6110339085612245565b5f8881526020902090915081015465ffffffffffff908116908716101561105c5780915061106a565b611067816001612258565b92505b505b5f611079878785856117de565b905080156110b35761109d87611090600184612245565b5f91825260209091200190565b54600160301b90046001600160d01b03166110b5565b5f5b979650505050505050565b5f6106d543611655565b6040516001600160a01b038481166024830152838116604483015260648201839052610ca79186918216906323b872dd90608401610bbb565b6001600160a01b03821661112c575f60405163ec442f0560e01b81526004016105d89190611ffe565b61060a5f8383611486565b6001600160a01b0381165f908152600960205260408120546106489061183d565b600b546001600160a01b03163314610880573360405163118cdaa760e01b81526004016105d89190611ffe565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f90815260076020526040812054610648565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600561186d565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600661186d565b6001600160a01b038216611276575f604051634b637e8f60e11b81526004016105d89190611ffe565b61060a825f83611486565b80545f9080156112b05761129a83611090600184612245565b54600160301b90046001600160d01b0316611009565b5f9392505050565b5f6106486112c4610e92565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112f488888888611916565b92509250925061130482826119d4565b50909695505050505050565b610bed83838360016113b4565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206106459083611a8c565b5f5f60205f8451602086015f885af180611370576040513d5f823e3d81fd5b50505f513d91508115611387578060011415611394565b6001600160a01b0384163b155b15610ca75783604051635274afe760e01b81526004016105d89190611ffe565b6001600160a01b0384166113dd575f60405163e602df0560e01b81526004016105d89190611ffe565b6001600160a01b038316611406575f604051634a1406b160e11b81526004016105d89190611ffe565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610ca757826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161147891815260200190565b60405180910390a350505050565b6001600160a01b038316158015906114a657506001600160a01b03821615155b156114c457604051638cd22d1960e01b815260040160405180910390fd5b610bed838383611af9565b6001600160a01b0381165f90815260208190526040812054610648565b816001600160a01b0316836001600160a01b03161415801561150d57505f81115b15610bed576001600160a01b038316156115b4576001600160a01b0383165f908152600960205260408120819061154f90611b5f61154a86611b6a565b611b9d565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115a9929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610bed576001600160a01b0382165f90815260096020526040812081906115ec90611bd561154a86611b6a565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611646929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff821115611687576040516306dfcc6560e41b815260306004820152602481018390526044016105d8565b5090565b5f60018211611698575090565b816001600160801b82106116b15760809190911c9060401b5b600160401b82106116c75760409190911c9060201b5b64010000000082106116de5760209190911c9060101b5b6201000082106116f35760109190911c9060081b5b61010082106117075760089190911c9060041b5b6010821061171a5760049190911c9060021b5b600482106117265760011b5b600302600190811c9081858161173e5761173e6122a7565b048201901c90506001818581611756576117566122a7565b048201901c9050600181858161176e5761176e6122a7565b048201901c90506001818581611786576117866122a7565b048201901c9050600181858161179e5761179e6122a7565b048201901c905060018185816117b6576117b66122a7565b048201901c90506117d58185816117cf576117cf6122a7565b04821190565b90039392505050565b5f5b81831015611835575f6117f38484611be0565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff1611156118215780925061182f565b61182c816001612258565b93505b506117e0565b509392505050565b5f63ffffffff821115611687576040516306dfcc6560e41b815260206004820152602481018390526044016105d8565b606060ff83146118875761188083611bfa565b9050610648565b818054611893906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546118bf906121f9565b801561190a5780601f106118e15761010080835404028352916020019161190a565b820191905f5260205f20905b8154815290600101906020018083116118ed57829003601f168201915b50505050509050610648565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561194557505f915060039050826119ca565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015611996573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b0381166119c157505f9250600191508290506119ca565b92505f91508190505b9450945094915050565b5f8260038111156119e7576119e76122bb565b036119f0575050565b6001826003811115611a0457611a046122bb565b03611a225760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115611a3657611a366122bb565b03611a575760405163fce698f760e01b8152600481018290526024016105d8565b6003826003811115611a6b57611a6b6122bb565b0361060a576040516335e2f38360e21b8152600481018290526024016105d8565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611ab957611ab96122cf565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611b04838383611c37565b6001600160a01b038316611b54575f611b1c60025490565b90506001600160d01b0380821115611b5157604051630e58ae9360e11b815260048101839052602481018290526044016105d8565b50505b610bed838383611d4a565b5f61064582846122e3565b5f6001600160d01b03821115611687576040516306dfcc6560e41b815260d06004820152602481018390526044016105d8565b5f5f611bc8611baa61093e565b611bc0611bb688611281565b868863ffffffff16565b879190611da9565b915091505b935093915050565b5f6106458284612302565b5f611bee6002848418612321565b61064590848416612258565b60605f611c0683611db6565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611c61578060025f828254611c569190612258565b90915550611cbe9050565b6001600160a01b0383165f9081526020819052604090205481811015611ca05783818360405163391434e360e21b81526004016105d89392919061226b565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611cda57600280548290039055611cf8565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611d3d91815260200190565b60405180910390a3505050565b6001600160a01b038316611d6c57611d69600a611bd561154a84611b6a565b50505b6001600160a01b038216611d8e57611d8b600a611b5f61154a84611b6a565b50505b610bed611d9a84610795565b611da384610795565b836114ec565b5f80611bc8858585611ddd565b5f60ff8216601f81111561064857604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611ed3575f611df987611090600185612245565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611e3c57604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611e7557825465ffffffffffff16600160301b6001600160d01b03891602178355611ec5565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611bcd92505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611bcd565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6106456020830184611f2d565b80356001600160a01b0381168114610e8d575f5ffd5b5f5f60408385031215611f94575f5ffd5b611f9d83611f6d565b946020939093013593505050565b5f5f5f60608486031215611fbd575f5ffd5b611fc684611f6d565b9250611fd460208501611f6d565b929592945050506040919091013590565b5f60208284031215611ff5575f5ffd5b61064582611f6d565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f61203060e0830189611f2d565b82810360408401526120428189611f2d565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015612097578351835260209384019390920191600101612079565b50909b9a5050505050505050505050565b5f602082840312156120b8575f5ffd5b5035919050565b60ff81168114610b8b575f5ffd5b5f5f5f5f5f5f60c087890312156120e2575f5ffd5b6120eb87611f6d565b955060208701359450604087013593506060870135612109816120bf565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a031215612139575f5ffd5b61214288611f6d565b965061215060208901611f6d565b95506040880135945060608801359350608088013561216e816120bf565b9699959850939692959460a0840135945060c09093013592915050565b5f5f6040838503121561219c575f5ffd5b6121a583611f6d565b91506121b360208401611f6d565b90509250929050565b5f5f604083850312156121cd575f5ffd5b6121d683611f6d565b9150602083013563ffffffff811681146121ee575f5ffd5b809150509250929050565b600181811c9082168061220d57607f821691505b60208210810361222b57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561064857610648612231565b8082018082111561064857610648612231565b6001600160a01b039390931683526020830191909152604082015260600190565b5f6020828403121561229c575f5ffd5b8151611009816120bf565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561064857610648612231565b6001600160d01b03818116838216019081111561064857610648612231565b5f8261233b57634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": { "3415": [ { "length": 32, - "start": 1988 + "start": 2115 }, { "length": 32, - "start": 2985 + "start": 3117 }, { "length": 32, - "start": 3290 + "start": 3422 }, { "length": 32, - "start": 3463 + "start": 3595 } ], "6684": [ { "length": 32, - "start": 3694 + "start": 3826 } ], "6686": [ { "length": 32, - "start": 3652 + "start": 3784 } ], "6688": [ { "length": 32, - "start": 3610 + "start": 3742 } ], "6690": [ { "length": 32, - "start": 3775 + "start": 3907 } ], "6692": [ { "length": 32, - "start": 3815 + "start": 3947 } ], "6695": [ { "length": 32, - "start": 4463 + "start": 4602 } ], "6698": [ { "length": 32, - "start": 4508 + "start": 4647 } ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json index 272ae5f3f1..49bba848aa 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "32281": [ + "33764": [ { "length": 32, "start": 91 @@ -164,13 +164,13 @@ "start": 11964 } ], - "32283": [ + "33766": [ { "length": 32, "start": 398 } ], - "32285": [ + "33768": [ { "length": 32, "start": 432 @@ -182,5 +182,5 @@ ] }, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json index 4bc1877086..77f4e0d705 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json @@ -396,5 +396,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index 5e9001b403..e3ed6a7e1a 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -39,11 +39,19 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { /// @notice Work value allocation configuration WorkValueAllocation internal _workAllocation; /// @notice Maps E3 ID to refund distribution - mapping(uint256 e3Id => RefundDistribution) internal _distributions; + mapping(uint256 e3Id => RefundDistribution distribution) + internal _distributions; /// @notice Tracks claims per E3 per address - mapping(uint256 e3Id => mapping(address => bool)) internal _claimed; + mapping(uint256 e3Id => mapping(address claimer => bool hasClaimed)) + internal _claimed; + /// @notice Tracks number of claims made per E3 (for routeSlashedFunds guard) + mapping(uint256 e3Id => uint256 count) internal _claimCount; + /// @notice Tracks number of honest node claims made per E3 (for dust fix) + mapping(uint256 e3Id => uint256 count) internal _honestNodeClaimCount; + /// @notice Tracks total amount paid to honest nodes per E3 (for dust fix) + mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses - mapping(uint256 e3Id => address[]) internal _honestNodes; + mapping(uint256 e3Id => address[] nodes) internal _honestNodes; //////////////////////////////////////////////////////////// // // // Modifiers // @@ -103,10 +111,12 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { function calculateRefund( uint256 e3Id, uint256 originalPayment, - address[] calldata honestNodes + address[] calldata honestNodes, + IERC20 paymentToken ) external onlyEnclave { require(!_distributions[e3Id].calculated, "Already calculated"); require(originalPayment > 0, "No payment"); + require(address(paymentToken) != address(0), "Invalid fee token"); // Calculate work value based on stage IEnclave.E3Stage failedAt = _getFailedAtStage(e3Id); @@ -121,14 +131,15 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { honestNodeAmount - requesterAmount; - // Store distribution + // Store distribution with the actual token used for this E3 _distributions[e3Id] = RefundDistribution({ requesterAmount: requesterAmount, honestNodeAmount: honestNodeAmount, protocolAmount: protocolAmount, totalSlashed: 0, honestNodeCount: honestNodes.length, - calculated: true + calculated: true, + feeToken: paymentToken }); // Store honest nodes @@ -138,7 +149,7 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { // Transfer protocol fee to treasury immediately if (protocolAmount > 0) { - feeToken.safeTransfer(treasury, protocolAmount); + paymentToken.safeTransfer(treasury, protocolAmount); } emit RefundDistributionCalculated( @@ -229,6 +240,12 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { RefundDistribution storage dist = _distributions[e3Id]; if (!dist.calculated) revert RefundNotCalculated(e3Id); + // Guard against pre-upgrade records where feeToken was not yet stored + require( + address(dist.feeToken) != address(0), + "feeToken not initialized" + ); + address requester = enclave.getRequester(e3Id); if (msg.sender != requester) revert NotRequester(e3Id, msg.sender); @@ -238,8 +255,10 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { if (amount == 0) revert NoRefundAvailable(e3Id); _claimed[e3Id][msg.sender] = true; + _claimCount[e3Id]++; - feeToken.safeTransfer(msg.sender, amount); + // Use the per-E3 fee token (not the global one, which may have been rotated) + dist.feeToken.safeTransfer(msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "REQUESTER"); } @@ -250,6 +269,13 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { ) external returns (uint256 amount) { RefundDistribution storage dist = _distributions[e3Id]; require(dist.calculated, RefundNotCalculated(e3Id)); + + // Guard against pre-upgrade records where feeToken was not yet stored + require( + address(dist.feeToken) != address(0), + "feeToken not initialized" + ); + require(!_claimed[e3Id][msg.sender], AlreadyClaimed(e3Id, msg.sender)); // Check if caller is honest node @@ -261,20 +287,26 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { require(isHonest, NotHonestNode(e3Id, msg.sender)); require(dist.honestNodeCount > 0, NoRefundAvailable(e3Id)); - amount = dist.honestNodeAmount / dist.honestNodeCount; + uint256 perNodeAmount = dist.honestNodeAmount / dist.honestNodeCount; + + _honestNodeClaimCount[e3Id]++; + if (_honestNodeClaimCount[e3Id] == dist.honestNodeCount) { + // Last claimer gets whatever remains (includes dust) + amount = dist.honestNodeAmount - _totalHonestNodePaid[e3Id]; + } else { + amount = perNodeAmount; + } + _totalHonestNodePaid[e3Id] += amount; require(amount > 0, NoRefundAvailable(e3Id)); _claimed[e3Id][msg.sender] = true; + _claimCount[e3Id]++; - // Distribute reward through bonding registry - feeToken.approve(address(bondingRegistry), amount); - - address[] memory nodeArray = new address[](1); - nodeArray[0] = msg.sender; - uint256[] memory amountArray = new uint256[](1); - amountArray[0] = amount; - - bondingRegistry.distributeRewards(feeToken, nodeArray, amountArray); + // Transfer directly to the honest node. Using distributeRewards would require + // this contract to be an authorized distributor in BondingRegistry, and the node + // must be registered. Direct transfer is simpler and more reliable for refunds. + IERC20 token = dist.feeToken; + token.safeTransfer(msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "HONEST_NODE"); } @@ -286,9 +318,10 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { ) external onlyEnclave { RefundDistribution storage dist = _distributions[e3Id]; require(dist.calculated, "Not calculated"); + require(_claimCount[e3Id] == 0, "Claims already started"); + require(amount > 0, "Zero amount"); // Add slashed funds to distribution - // Note: slashing should be finalized before claims are made. // 50% to requester, 50% to honest nodes for non-participation uint256 toRequester = amount / 2; uint256 toHonestNodes = amount - toRequester; diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 16206aaf22..d5e8abbbd6 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; +import { ISlashingManager } from "./interfaces/ISlashingManager.sol"; import { IE3RefundManager } from "./interfaces/IE3RefundManager.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { @@ -23,6 +24,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @notice Main contract for managing Encrypted Execution Environments (E3) * @dev Coordinates E3 lifecycle including request, activation, input publishing, and output verification */ +// solhint-disable-next-line max-states-count contract Enclave is IEnclave, OwnableUpgradeable { using SafeERC20 for IERC20; @@ -44,6 +46,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Manages refund calculation and claiming for failed E3s. IE3RefundManager public e3RefundManager; + /// @notice Slashing Manager contract for fault attribution. + /// @dev Used to check which operators have been slashed for E3s. + ISlashingManager public slashingManager; + /// @notice Address of the ERC20 token used for E3 fees. /// @dev All E3 request fees must be paid in this token. IERC20 public feeToken; @@ -78,16 +84,19 @@ contract Enclave is IEnclave, OwnableUpgradeable { mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; /// @notice Maps E3 ID to its current stage - mapping(uint256 e3Id => E3Stage) internal _e3Stages; + mapping(uint256 e3Id => E3Stage stage) internal _e3Stages; /// @notice Maps E3 ID to its deadlines - mapping(uint256 e3Id => E3Deadlines) internal _e3Deadlines; + mapping(uint256 e3Id => E3Deadlines deadlines) internal _e3Deadlines; /// @notice Maps E3 ID to failure reason (if failed) - mapping(uint256 e3Id => FailureReason) internal _e3FailureReasons; + mapping(uint256 e3Id => FailureReason reason) internal _e3FailureReasons; /// @notice Maps E3 ID to requester address - mapping(uint256 e3Id => address) internal _e3Requesters; + mapping(uint256 e3Id => address requester) internal _e3Requesters; + + /// @notice Maps E3 ID to the fee token used at request time + mapping(uint256 e3Id => IERC20 token) internal _e3FeeTokens; /// @notice Global timeout configuration E3TimeoutConfig internal _timeoutConfig; @@ -206,6 +215,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { _; } + /// @notice Restricts function to CiphernodeRegistry or SlashingManager + modifier onlyCiphernodeRegistryOrSlashingManager() { + require( + msg.sender == address(ciphernodeRegistry) || + msg.sender == address(slashingManager), + "Only Registry or SlashingMgr" + ); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -336,6 +355,9 @@ contract Enclave is IEnclave, OwnableUpgradeable { feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); + // Store the fee token used for this E3 (survives global token rotations) + _e3FeeTokens[e3Id] = feeToken; + require( ciphernodeRegistry.requestCommittee( e3Id, @@ -366,6 +388,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { ) external returns (bool success) { E3 memory e3 = getE3(e3Id); + E3Stage current = _e3Stages[e3Id]; + require( + current == E3Stage.KeyPublished, + InvalidStage(e3Id, E3Stage.KeyPublished, current) + ); + E3Deadlines memory deadlines = _e3Deadlines[e3Id]; // You cannot post outputs after the compute deadline @@ -454,39 +482,57 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - /// @notice Distributes rewards to committee members after successful E3 completion. - /// @dev Divides the E3 payment equally among all committee members and transfers via bonding registry. - /// @dev Emits RewardsDistributed event upon successful distribution. + /// @notice Distributes rewards to active committee members after successful E3 completion. + /// @dev Uses active committee nodes (excluding expelled members). + /// Divides the E3 payment equally among active members and transfers via bonding registry. + /// If no active members remain (e.g., all expelled), refunds the requester to prevent fund lockup. + /// Any division dust is sent to the last member rather than being lost. /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { - address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( - e3Id - ); - uint256 committeeLength = committeeNodes.length; - uint256[] memory amounts = new uint256[](committeeLength); - - // TODO: do we need to pay different amounts to different nodes? - // For now, we'll pay the same amount to all nodes. - uint256 amount = e3Payments[e3Id] / committeeLength; - for (uint256 i = 0; i < committeeLength; i++) { - amounts[i] = amount; - } + address[] memory activeNodes = ciphernodeRegistry + .getActiveCommitteeNodes(e3Id); + uint256 activeLength = activeNodes.length; uint256 totalAmount = e3Payments[e3Id]; e3Payments[e3Id] = 0; + if (totalAmount == 0) return; + + // Use the per-E3 fee token (not the global one, which may have been rotated) + IERC20 paymentToken = _e3FeeTokens[e3Id]; - feeToken.approve(address(bondingRegistry), totalAmount); + if (activeLength == 0) { + address requester = _e3Requesters[e3Id]; + if (requester != address(0)) { + paymentToken.safeTransfer(requester, totalAmount); + } + return; + } + + uint256[] memory amounts = new uint256[](activeLength); - bondingRegistry.distributeRewards(feeToken, committeeNodes, amounts); + // Distribute equally among active (non-expelled) committee members + uint256 amount = totalAmount / activeLength; + uint256 distributed = 0; + for (uint256 i = 0; i < activeLength; i++) { + amounts[i] = amount; + distributed += amount; + } + uint256 dust = totalAmount - distributed; + if (dust > 0) { + amounts[activeLength - 1] += dust; + } - // TODO: decide where does dust go? Treasury maybe? - feeToken.approve(address(bondingRegistry), 0); + paymentToken.forceApprove(address(bondingRegistry), totalAmount); - emit RewardsDistributed(e3Id, committeeNodes, amounts); + bondingRegistry.distributeRewards(paymentToken, activeNodes, amounts); + + paymentToken.forceApprove(address(bondingRegistry), 0); + + emit RewardsDistributed(e3Id, activeNodes, amounts); } /// @notice Retrieves the honest committee nodes for a given E3. - /// @dev Determines honest nodes based on failure reason and committee publication status. + /// @dev Uses active committee view from the registry (which excludes expelled/slashed members). /// @param e3Id The ID of the E3. /// @return honestNodes An array of addresses of honest committee nodes. function _getHonestNodes( @@ -502,12 +548,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { return new address[](0); } - // Try to get published committee nodes - try ciphernodeRegistry.getCommitteeNodes(e3Id) returns ( + // Use active committee nodes (already filtered by expulsion) + try ciphernodeRegistry.getActiveCommitteeNodes(e3Id) returns ( address[] memory nodes ) { - // TODO: Implement fault attribution to filter honest from faulting nodes - return nodes; // Assume all are honest for now + return nodes; } catch { return new address[](0); // Committee not published (DKG failed) } @@ -621,6 +666,21 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit AllowedE3ProgramsParamsSet(_e3ProgramsParams); } + /// @notice Removes previously allowed E3 program parameter sets + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to remove + function removeE3ProgramsParams( + bytes[] memory _e3ProgramsParams + ) public onlyOwner { + uint256 length = _e3ProgramsParams.length; + for (uint256 i; i < length; ) { + delete e3ProgramsParams[_e3ProgramsParams[i]]; + unchecked { + ++i; + } + } + emit E3ProgramsParamsRemoved(_e3ProgramsParams); + } + /// @notice Sets the E3 Refund Manager contract address /// @param _e3RefundManager The new E3 Refund Manager contract address function setE3RefundManager( @@ -634,8 +694,22 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3RefundManagerSet(address(_e3RefundManager)); } + /// @notice Sets the Slashing Manager contract address + /// @param _slashingManager The new Slashing Manager contract address + function setSlashingManager( + ISlashingManager _slashingManager + ) public onlyOwner { + require( + address(_slashingManager) != address(0), + "Invalid SlashingManager address" + ); + slashingManager = _slashingManager; + emit SlashingManagerSet(address(_slashingManager)); + } + /// @notice Process a failed E3 and calculate refunds - /// @dev Can be called by anyone once E3 is in failed state + /// @dev Can be called by anyone once E3 is in failed state. + /// Uses the per-E3 feeToken stored at request time (survives global token rotation). /// @param e3Id The ID of the failed E3 function processE3Failure(uint256 e3Id) external { E3Stage stage = _e3Stages[e3Id]; @@ -647,8 +721,15 @@ contract Enclave is IEnclave, OwnableUpgradeable { address[] memory honestNodes = _getHonestNodes(e3Id); - feeToken.safeTransfer(address(e3RefundManager), payment); - e3RefundManager.calculateRefund(e3Id, payment, honestNodes); + IERC20 paymentToken = _e3FeeTokens[e3Id]; + + paymentToken.safeTransfer(address(e3RefundManager), payment); + e3RefundManager.calculateRefund( + e3Id, + payment, + honestNodes, + paymentToken + ); emit E3FailureProcessed(e3Id, payment, honestNodes.length); } @@ -701,8 +782,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { function onE3Failed( uint256 e3Id, uint8 reason - ) external onlyCiphernodeRegistry { - require(reason > 0 && reason <= 12, "Invalid failure reason"); + ) external onlyCiphernodeRegistryOrSlashingManager { + require( + reason > 0 && reason <= uint8(FailureReason.VerificationFailed), + "Invalid failure reason" + ); // Mark E3 as failed with the given reason _markE3FailedWithReason(e3Id, FailureReason(reason)); } @@ -861,7 +945,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { require(config.dkgWindow > 0, "Invalid DKG window"); require(config.computeWindow > 0, "Invalid compute window"); require(config.decryptionWindow > 0, "Invalid decryption window"); - require(config.gracePeriod > 0, "Invalid grace period"); _timeoutConfig = config; diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index a343b2f747..d910cdb5ff 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -350,7 +350,7 @@ interface IBondingRegistry { * @param rewardToken Reward token contract * @param operators Addresses of the operators to distribute rewards to * @param amounts Amounts of rewards to distribute to each operator - * @dev Only callable by contract owner + * @dev Only callable by authorized distributors. */ function distributeRewards( IERC20 rewardToken, @@ -439,6 +439,13 @@ interface IBondingRegistry { */ function setRewardDistributor(address newRewardDistributor) external; + /** + * @notice Revoke reward distributor authorization + * @param distributor Address to revoke + * @dev Only callable by contract owner + */ + function revokeRewardDistributor(address distributor) external; + /** * @notice Withdraw slashed funds to treasury * @param ticketAmount Amount of slashed ticket balance to withdraw diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 4cfa7b1c8f..69b12a8752 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -41,6 +41,8 @@ interface ICiphernodeRegistry { address[] committee; mapping(address node => bool submitted) submitted; mapping(address node => uint256 score) scoreOf; + mapping(address node => bool active) active; + uint256 activeCount; } /// @notice This event MUST be emitted when a committee is selected for an E3. @@ -98,6 +100,30 @@ interface ICiphernodeRegistry { /// @param active True if committee is now active, false if completed. event CommitteeActivationChanged(uint256 indexed e3Id, bool active); + /// @notice This event MUST be emitted when a committee member is expelled due to slashing. + /// @param e3Id ID of the E3 for which the member was expelled. + /// @param node Address of the expelled committee member. + /// @param reason Hash of the slash reason that caused the expulsion. + /// @param activeCountAfter Number of active committee members remaining after expulsion. + event CommitteeMemberExpelled( + uint256 indexed e3Id, + address indexed node, + bytes32 reason, + uint256 activeCountAfter + ); + + /// @notice This event MUST be emitted when committee viability changes after an expulsion. + /// @param e3Id ID of the E3. + /// @param activeCount Current number of active committee members. + /// @param thresholdM The minimum threshold (M) required. + /// @param viable Whether the committee is still viable (activeCount >= M). + event CommitteeViabilityUpdated( + uint256 indexed e3Id, + uint256 activeCount, + uint256 thresholdM, + bool viable + ); + /// @notice This event MUST be emitted when `enclave` is set. /// @param enclave Address of the enclave contract. event EnclaveSet(address indexed enclave); @@ -249,4 +275,49 @@ interface ICiphernodeRegistry { /// @param e3Id ID of the E3 computation /// @return committeeDeadline The committee deadline timestamp function getCommitteeDeadline(uint256 e3Id) external view returns (uint256); + + /// @notice Expel a committee member from a specific E3 committee due to slashing + /// @dev Only callable by SlashingManager. Idempotent (re-expelling same member is no-op). + /// Returns viability data so the caller can decide whether to fail the E3 — + /// eliminating the need for separate getActiveCommitteeCount/getCommitteeThreshold calls. + /// @param e3Id ID of the E3 computation + /// @param node Address of the committee member to expel + /// @param reason Hash of the slash reason + /// @return activeCount Number of active committee members after expulsion + /// @return thresholdM The minimum threshold (M) required for viability + function expelCommitteeMember( + uint256 e3Id, + address node, + bytes32 reason + ) external returns (uint256 activeCount, uint32 thresholdM); + + /// @notice Check if a committee member is still active for a specific E3 + /// @param e3Id ID of the E3 computation + /// @param node Address of the committee member to check + /// @return active Whether the member is still active in the committee + function isCommitteeMemberActive( + uint256 e3Id, + address node + ) external view returns (bool active); + + /// @notice Get active (non-expelled) committee nodes for an E3 + /// @param e3Id ID of the E3 computation + /// @return nodes Array of active committee member addresses + function getActiveCommitteeNodes( + uint256 e3Id + ) external view returns (address[] memory nodes); + + /// @notice Get the count of active committee members for an E3 + /// @param e3Id ID of the E3 computation + /// @return count Number of active committee members + function getActiveCommitteeCount( + uint256 e3Id + ) external view returns (uint256 count); + + /// @notice Get the threshold configuration for an E3 committee + /// @param e3Id ID of the E3 computation + /// @return threshold The [M, N] threshold array + function getCommitteeThreshold( + uint256 e3Id + ) external view returns (uint32[2] memory threshold); } diff --git a/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol new file mode 100644 index 0000000000..ffca72f19b --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity >=0.8.27; + +/** + * @title ICircuitVerifier + * @notice Interface for on-chain ZK circuit verifiers (e.g., DkgPkVerifier, Honk verifiers) + * @dev Standard interface matching the verification pattern used by Honk-generated verifiers. + * Set the circuit verifier address directly as the proofVerifier in a SlashPolicy. + */ +interface ICircuitVerifier { + /// @notice Verify a ZK proof against public inputs + /// @param _proof The raw proof bytes + /// @param _publicInputs The public inputs to verify against + /// @return True if the proof is valid + function verify( + bytes calldata _proof, + bytes32[] calldata _publicInputs + ) external returns (bool); +} diff --git a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol index aa2062cb47..44a079494b 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; import { IEnclave } from "./IEnclave.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title IE3RefundManager @@ -32,6 +33,7 @@ interface IE3RefundManager { uint256 totalSlashed; // Slashed funds added uint256 honestNodeCount; // Number of honest nodes bool calculated; // Whether distribution is calculated + IERC20 feeToken; // The fee token used for this E3's payment (stored per-E3 to survive token rotations) } //////////////////////////////////////////////////////////// // // @@ -86,10 +88,12 @@ interface IE3RefundManager { /// @param e3Id The failed E3 ID /// @param originalPayment The original payment amount /// @param honestNodes Array of honest node addresses + /// @param paymentToken The fee token that was used for this E3's payment function calculateRefund( uint256 e3Id, uint256 originalPayment, - address[] calldata honestNodes + address[] calldata honestNodes, + IERC20 paymentToken ) external; /// @notice Requester claims their refund diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 67a0a81382..f96d76a2a0 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -59,7 +59,6 @@ interface IEnclave { uint256 dkgWindow; uint256 computeWindow; uint256 decryptionWindow; - uint256 gracePeriod; } /// @notice Deadlines for each E3 @@ -153,10 +152,18 @@ interface IEnclave { /// @param e3ProgramParams Array of encoded encryption scheme parameters (e.g, for BFV) event AllowedE3ProgramsParamsSet(bytes[] e3ProgramParams); + /// @notice Emitted when E3 program parameter sets are removed. + /// @param e3ProgramParams Array of removed encryption scheme parameters. + event E3ProgramsParamsRemoved(bytes[] e3ProgramParams); + /// @notice Emitted when E3RefundManager contract is set. /// @param e3RefundManager The address of the E3RefundManager contract. event E3RefundManagerSet(address indexed e3RefundManager); + /// @notice Emitted when the SlashingManager contract is set. + /// @param slashingManager The address of the SlashingManager contract. + event SlashingManagerSet(address indexed slashingManager); + /// @notice Emitted when a failed E3 is processed for refunds. /// @param e3Id The ID of the failed E3. /// @param paymentAmount The original payment amount being refunded. @@ -307,6 +314,11 @@ interface IEnclave { /// @param _e3ProgramsParams Array of ABI encoded parameter sets to allow. function setE3ProgramsParams(bytes[] memory _e3ProgramsParams) external; + /// @notice Removes previously allowed E3 program parameter sets. + /// @dev This function revokes specific parameter sets that should no longer be allowed. + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to remove. + function removeE3ProgramsParams(bytes[] memory _e3ProgramsParams) external; + //////////////////////////////////////////////////////////// // // // Get Functions // diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol deleted file mode 100644 index a2ac4b860f..0000000000 --- a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -pragma solidity >=0.8.27; - -/** - * @title ISlashVerifier - * @notice Interface for verifying slash proofs - * @dev Slash verifiers implement cryptographic or logical verification of slash proposals - */ -interface ISlashVerifier { - /// @notice Verify a slash proof - /// @dev This function is called by the SlashingManager contract during slash proposal to verify proof validity - /// @param proposalId ID of the slash proposal - /// @param proof ABI encoded proof data supporting the slash - /// @return success Whether the proof was successfully verified - function verify( - uint256 proposalId, - bytes memory proof - ) external view returns (bool success); -} diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index d5711fc77b..9cf3c4089d 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -11,7 +11,9 @@ import { IBondingRegistry } from "./IBondingRegistry.sol"; /** * @title ISlashingManager * @notice Interface for managing slashing proposals, appeals, and execution - * @dev Maintains policy table and handles slash workflows with appeals + * @dev Maintains policy table and handles slash workflows with two lanes: + * Lane A (proof-based): permissionless, atomic, no appeals + * Lane B (evidence-based): SLASHER_ROLE required, appeal window */ interface ISlashingManager { // ====================== @@ -28,6 +30,8 @@ interface ISlashingManager { * @param banNode Whether executing this slash will permanently ban the node * @param appealWindow Time window in seconds for operators to appeal (0 = immediate execution, no appeals) * @param enabled Whether this slash type is currently active and can be proposed + * @param affectsCommittee Whether executing this slash triggers committee expulsion for the target E3 + * @param failureReason The FailureReason enum value to use when committee drops below threshold (0 = no E3 failure) */ struct SlashPolicy { uint256 ticketPenalty; @@ -37,11 +41,14 @@ interface ISlashingManager { bool banNode; uint256 appealWindow; bool enabled; + bool affectsCommittee; + uint8 failureReason; } /** * @notice Slash proposal details tracking the full lifecycle of a slash * @dev Stores all state needed for proposal, appeal, and execution workflows + * @param e3Id ID of the E3 computation this slash relates to (0 for non-E3 slashes) * @param operator Address of the ciphernode operator being slashed * @param reason Hash of the slash reason (maps to SlashPolicy configuration) * @param ticketAmount Amount of ticket collateral to slash (copied from policy at proposal time) @@ -57,6 +64,7 @@ interface ISlashingManager { * @param proofVerified Whether the proof was successfully verified by the proof verifier contract */ struct SlashProposal { + uint256 e3Id; address operator; bytes32 reason; uint256 ticketAmount; @@ -70,6 +78,12 @@ interface ISlashingManager { address proposer; bytes32 proofHash; bool proofVerified; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + bool banNode; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + bool affectsCommittee; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + uint8 failureReason; } // ====================== @@ -94,6 +108,21 @@ interface ISlashingManager { /// @notice Thrown when provided proof fails verification error InvalidProof(); + /// @notice The ZK proof verified successfully — the operator's submission was valid, not a fault + error ProofIsValid(); + + /// @notice Thrown when the recovered signer does not match the operator being slashed + error SignerIsNotOperator(); + + /// @notice Thrown when the operator is not a member of the committee for this E3 + error OperatorNotInCommittee(); + + /// @notice Thrown when the verifier address in signed evidence doesn't match the policy's current verifier + error VerifierMismatch(); + + /// @notice Thrown when the verifier staticcall fails (e.g., contract doesn't exist, reverts, or runs out of gas) + error VerifierCallFailed(); + /// @notice Thrown when attempting to execute a slash whose appeal was upheld error AppealUpheld(); @@ -127,6 +156,12 @@ interface ISlashingManager { /// @notice Thrown when a policy requires proof but no verifier contract is configured error VerifierNotSet(); + /// @notice Thrown when the same evidence bundle has already been used in a proposal + error DuplicateEvidence(); + + /// @notice Thrown when the chainId in the signed proof payload does not match the current chain + error ChainIdMismatch(); + // ====================== // Events // ====================== @@ -141,6 +176,7 @@ interface ISlashingManager { /** * @notice Emitted when a new slash proposal is created * @param proposalId Unique ID of the created proposal + * @param e3Id ID of the E3 computation related to this slash * @param operator Address of the ciphernode operator being slashed * @param reason Hash of the slash reason * @param ticketAmount Amount of ticket collateral to be slashed @@ -150,8 +186,9 @@ interface ISlashingManager { */ event SlashProposed( uint256 indexed proposalId, + uint256 indexed e3Id, address indexed operator, - bytes32 indexed reason, + bytes32 reason, uint256 ticketAmount, uint256 licenseAmount, uint256 executableAt, @@ -161,6 +198,7 @@ interface ISlashingManager { /** * @notice Emitted when a slash proposal is executed and penalties are applied * @param proposalId ID of the executed proposal + * @param e3Id ID of the E3 committee associated with this slash * @param operator Address of the slashed operator * @param reason Hash of the slash reason * @param ticketAmount Amount of ticket collateral slashed @@ -169,6 +207,7 @@ interface ISlashingManager { */ event SlashExecuted( uint256 indexed proposalId, + uint256 e3Id, address indexed operator, bytes32 indexed reason, uint256 ticketAmount, @@ -295,7 +334,7 @@ interface ISlashingManager { /** * @notice Grants SLASHER_ROLE to an address - * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute slashes + * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute evidence-based slashes * @param slasher Address to grant slashing permissions (must be non-zero) */ function addSlasher(address slasher) external; @@ -307,58 +346,58 @@ interface ISlashingManager { */ function removeSlasher(address slasher) external; - /** - * @notice Grants VERIFIER_ROLE to an address - * @dev Only callable by DEFAULT_ADMIN_ROLE. Verifiers can validate proof-based slashes - * @param verifier Address to grant verification permissions (must be non-zero) - */ - function addVerifier(address verifier) external; - - /** - * @notice Revokes VERIFIER_ROLE from an address - * @dev Only callable by DEFAULT_ADMIN_ROLE - * @param verifier Address to revoke verification permissions from - */ - function removeVerifier(address verifier) external; - // ====================== // Slashing Functions // ====================== /** - * @notice Creates a new slash proposal against an operator - * @dev Only callable by SLASHER_ROLE. Validates policy and proof if required + * @notice Creates a new slash proposal with cryptographic proof (Lane A - permissionless) + * @dev Anyone can call this for proof-based slashes. Requires the operator's ECDSA signature + * over the proof payload to prevent arbitrary slashing. + * Evidence format: + * abi.encode(bytes zkProof, bytes32[] publicInputs, + * bytes signature, uint256 chainId, uint256 proofType, address verifier) + * The operator must have signed: keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, chainId, e3Id, + * proofType, keccak256(zkProof), keccak256(abi.encodePacked(publicInputs)))) + * Verifications performed: + * 1. Verifier address in evidence matches the policy's current proofVerifier + * 2. Signature recovery confirms the operator authored the bad proof + * 3. Committee membership check confirms the operator was in the E3's committee + * 4. ZK proof re-verification confirms the proof is indeed invalid (fault) + * @param e3Id ID of the E3 computation this slash relates to * @param operator Address of the ciphernode operator to slash (must be non-zero) - * @param reason Hash of the slash reason (must have an enabled policy configured) - * @param proof Proof data to be verified (required if policy.requiresProof is true, can be empty otherwise) + * @param reason Hash of the slash reason (must have an enabled proof-required policy) + * @param proof Evidence data: abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) * @return proposalId Sequential ID of the created proposal - * Requirements: - * - operator must not be zero address - * - reason must have an enabled policy configured - * - If policy requires proof, proof must be non-empty and pass verification - * - Caller must have SLASHER_ROLE */ function proposeSlash( + uint256 e3Id, address operator, bytes32 reason, bytes calldata proof ) external returns (uint256 proposalId); + /** + * @notice Creates a new slash proposal with evidence (Lane B - SLASHER_ROLE required) + * @dev Only callable by SLASHER_ROLE. Evidence-based slashes have appeal windows. + * @param e3Id ID of the E3 computation this slash relates to + * @param operator Address of the ciphernode operator to slash (must be non-zero) + * @param reason Hash of the slash reason (must have an enabled non-proof policy) + * @param evidence Evidence data supporting the slash proposal + * @return proposalId Sequential ID of the created proposal + */ + function proposeSlashEvidence( + uint256 e3Id, + address operator, + bytes32 reason, + bytes calldata evidence + ) external returns (uint256 proposalId); + /** * @notice Executes a slash proposal and applies penalties to the operator - * @dev Only callable by SLASHER_ROLE. Validates execution conditions and applies slashing + * @dev For evidence-based slashes, validates appeal window has expired. + * Proof-based slashes are executed atomically in proposeSlash. * @param proposalId ID of the proposal to execute (must exist and not be already executed) - * Requirements: - * - Proposal must exist and not be already executed - * - For proof-required slashes: proof must be verified - * - For evidence-based slashes: appeal window must have expired - * - If appeal was filed and resolved, appeal must not have been upheld - * - Caller must have SLASHER_ROLE - * Effects: - * - Marks proposal as executed - * - Slashes ticket balance if ticketAmount > 0 - * - Slashes license bond if licenseAmount > 0 - * - Bans node if policy.banNode is true */ function executeSlash(uint256 proposalId) external; @@ -367,32 +406,19 @@ interface ISlashingManager { // ====================== /** - * @notice Allows an operator to file an appeal against a slash proposal + * @notice Allows an operator to file an appeal against an evidence-based slash proposal * @dev Only the operator being slashed can file an appeal, and only within the appeal window * @param proposalId ID of the proposal to appeal (must exist) * @param evidence String containing evidence and arguments supporting the appeal - * Requirements: - * - Proposal must exist - * - Caller must be the operator being slashed - * - Current timestamp must be before proposal.executableAt (within appeal window) - * - Proposal must not already have an appeal filed */ function fileAppeal(uint256 proposalId, string calldata evidence) external; /** * @notice Resolves an appeal by accepting or rejecting it * @dev Only callable by GOVERNANCE_ROLE. If appeal is upheld, the slash cannot be executed - * @param proposalId ID of the proposal with the appeal to resolve (must exist and have an appeal) - * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny the appeal - * (allow slash to proceed) + * @param proposalId ID of the proposal with the appeal to resolve + * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny * @param resolution String explaining the governance decision - * Requirements: - * - Proposal must exist and have an appeal filed - * - Appeal must not already be resolved - * - Caller must have GOVERNANCE_ROLE - * Effects: - * - Marks appeal as resolved - * - Sets appealUpheld flag (true = slash cancelled, false = slash can proceed) */ function resolveAppeal( uint256 proposalId, @@ -410,13 +436,6 @@ interface ISlashingManager { * @param node Address of the node to ban (must be non-zero) * @param status Whether to ban the node * @param reason Hash of the reason for banning - * Requirements: - * - node must not be zero address - * - Caller must have GOVERNANCE_ROLE - * Effects: - * - Sets banned[node] to status - * - Emits NodeBanned event if status is true - * - Emits NodeUnbanned event if status is false */ function updateBanStatus( address node, diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 41006ce38b..53eff07658 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -9,6 +9,9 @@ pragma solidity >=0.8.27; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + ReentrancyGuardUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 @@ -26,7 +29,12 @@ import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; * @notice Implementation of the bonding registry managing operator ticket balances and license bonds * @dev Handles deposits, withdrawals, slashing, exits, and integrates with registry and slashing manager */ -contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { +// solhint-disable-next-line max-states-count +contract BondingRegistry is + IBondingRegistry, + OwnableUpgradeable, + ReentrancyGuardUpgradeable +{ using SafeERC20 for IERC20; using ExitQueueLib for ExitQueueLib.ExitQueueState; @@ -62,8 +70,11 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @notice Address authorized to perform slashing operations address public slashingManager; - /// @notice Address authorized to distribute rewards to operators - address public rewardDistributor; + /// @notice Addresses authorized to distribute rewards to operators + /// @dev Multiple contracts (Enclave, E3RefundManager) need to distribute rewards. + /// Each authorized distributor must approve this contract for the reward token. + mapping(address distributor => bool authorized) + public authorizedDistributors; /// @notice Treasury address that receives slashed funds address public slashedFundsTreasury; @@ -173,6 +184,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { uint64 _exitDelay ) public initializer { __Ownable_init(msg.sender); + __ReentrancyGuard_init(); setTicketToken(_ticketToken); setLicenseToken(_licenseToken); setRegistry(_registry); @@ -304,7 +316,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function deregisterOperator( uint256[] calldata siblingNodes - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { Operator storage op = operators[msg.sender]; require(op.registered, NotRegistered()); @@ -352,7 +364,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function addTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); @@ -371,7 +383,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function removeTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -393,7 +405,9 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { } /// @inheritdoc IBondingRegistry - function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { + function bondLicense( + uint256 amount + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); uint256 balanceBefore = licenseToken.balanceOf(address(this)); @@ -416,7 +430,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function unbondLicense( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -444,7 +458,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount - ) external { + ) external nonReentrant { (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, maxTicketAmount, @@ -573,15 +587,15 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { IERC20 rewardToken, address[] calldata recipients, uint256[] calldata amounts - ) external { - require(msg.sender == rewardDistributor, OnlyRewardDistributor()); + ) external nonReentrant { + require(authorizedDistributors[msg.sender], OnlyRewardDistributor()); require(recipients.length == amounts.length, ArrayLengthMismatch()); uint256 len = recipients.length; for (uint256 i = 0; i < len; i++) { - if (amounts[i] > 0 && operators[recipients[i]].registered) { + if (amounts[i] > 0) { rewardToken.safeTransferFrom( - rewardDistributor, + msg.sender, recipients[i], amounts[i] ); @@ -679,20 +693,27 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { slashingManager = newSlashingManager; } - /// @notice Sets the reward distributor address - /// @dev Only callable by owner - /// @param newRewardDistributor Address of the reward distributor + /// @notice Authorizes an address to distribute rewards + /// @dev Only callable by owner. Supports multiple authorized distributors (Enclave + E3RefundManager) + /// @param newRewardDistributor Address to authorize as reward distributor function setRewardDistributor( address newRewardDistributor ) public onlyOwner { - rewardDistributor = newRewardDistributor; + authorizedDistributors[newRewardDistributor] = true; + } + + /// @notice Revokes reward distributor authorization + /// @dev Only callable by owner + /// @param distributor Address to revoke + function revokeRewardDistributor(address distributor) public onlyOwner { + authorizedDistributors[distributor] = false; } /// @inheritdoc IBondingRegistry function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount - ) public onlyOwner { + ) public onlyOwner nonReentrant { require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 298ab77f9e..fc20835afd 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { IEnclave } from "../interfaces/IEnclave.sol"; +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -66,6 +67,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Maps E3 ID to its committee data mapping(uint256 e3Id => Committee committee) internal committees; + /// @notice Address of the slashing manager authorized to expel committee members + ISlashingManager public slashingManager; + //////////////////////////////////////////////////////////// // // // Errors // @@ -143,6 +147,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Caller is not authorized error Unauthorized(); + /// @notice Caller is not the slashing manager + error NotSlashingManager(); + /// @notice Not enough registered ciphernodes to meet threshold /// @param requested The requested committee size (N) /// @param available The number of registered ciphernodes @@ -175,6 +182,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { _; } + /// @dev Restricts function access to only the slashing manager + modifier onlySlashingManager() { + require(msg.sender == address(slashingManager), NotSlashingManager()); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -213,6 +226,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { //////////////////////////////////////////////////////////// /// @inheritdoc ICiphernodeRegistry + /// @dev Uses numActiveOperators() which checks registered + minimum bond + minimum tickets. + /// Between request time and ticket submission, operators may become inactive by losing + /// bond or tickets. The check at request time may be stale by the time submitTicket + /// is called. This is appropriately conservative — it prevents requesting committees + /// when not enough operators are active even at request time. function requestCommittee( uint256 e3Id, uint256 seed, @@ -266,6 +284,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // TODO: Currently we trust the owner to publish the correct committee. // TODO: Need a Proof that the public key is generated from the committee + // SECURITY: Without DKG correctness proofs, a malicious owner could publish a key they + // control, enabling decryption of all E3 results. This is a centralization assumption + // accepted for the current phase. DKG proof verification must be added before + // decentralizing the owner role. c.publicKey = publicKeyHash; publicKeyHashes[e3Id] = publicKeyHash; // Progress E3 to KeyPublished stage @@ -354,7 +376,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(c.initialized, CommitteeNotRequested()); require(!c.finalized, CommitteeAlreadyFinalized()); require( - block.timestamp >= c.committeeDeadline, + block.timestamp > c.committeeDeadline, SubmissionWindowNotClosed() ); c.finalized = true; @@ -375,6 +397,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } c.committee = c.topNodes; + // Initialize active committee tracking in Committee struct + uint256 committeeLen = c.committee.length; + for (uint256 i = 0; i < committeeLen; ) { + c.active[c.committee[i]] = true; + unchecked { + ++i; + } + } + c.activeCount = committeeLen; + enclave.onCommitteeFinalized(e3Id); emit CommitteeFinalized(e3Id, c.topNodes); return true; @@ -406,6 +438,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit BondingRegistrySet(address(_bondingRegistry)); } + /// @notice Sets the slashing manager contract address + /// @dev Only callable by owner + /// @param _slashingManager Address of the slashing manager contract + function setSlashingManager( + ISlashingManager _slashingManager + ) public onlyOwner { + require(address(_slashingManager) != address(0), ZeroAddress()); + slashingManager = _slashingManager; + } + /// @inheritdoc ICiphernodeRegistry function setSortitionSubmissionWindow( uint256 _sortitionSubmissionWindow @@ -496,6 +538,87 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return c.committeeDeadline; } + //////////////////////////////////////////////////////////// + // // + // Committee Expulsion Functions // + // // + //////////////////////////////////////////////////////////// + + /// @inheritdoc ICiphernodeRegistry + function expelCommitteeMember( + uint256 e3Id, + address node, + bytes32 reason + ) + external + onlySlashingManager + returns (uint256 activeCount, uint32 thresholdM) + { + Committee storage c = committees[e3Id]; + require(c.finalized, CommitteeNotFinalized()); + thresholdM = c.threshold[0]; + + // Idempotent: if already expelled, return current state + if (!c.active[node]) { + activeCount = c.activeCount; + return (activeCount, thresholdM); + } + + c.active[node] = false; + c.activeCount--; + + activeCount = c.activeCount; + emit CommitteeMemberExpelled(e3Id, node, reason, activeCount); + + // Emit viability update + bool viable = activeCount >= thresholdM; + emit CommitteeViabilityUpdated(e3Id, activeCount, thresholdM, viable); + } + + /// @inheritdoc ICiphernodeRegistry + function isCommitteeMemberActive( + uint256 e3Id, + address node + ) external view returns (bool) { + return committees[e3Id].active[node]; + } + + /// @inheritdoc ICiphernodeRegistry + function getActiveCommitteeNodes( + uint256 e3Id + ) external view returns (address[] memory) { + Committee storage c = committees[e3Id]; + uint256 total = c.committee.length; + uint256 actCount = c.activeCount; + + address[] memory activeNodes = new address[](actCount); + uint256 idx = 0; + for (uint256 i = 0; i < total; ) { + if (c.active[c.committee[i]]) { + activeNodes[idx] = c.committee[i]; + idx++; + } + unchecked { + ++i; + } + } + return activeNodes; + } + + /// @inheritdoc ICiphernodeRegistry + function getActiveCommitteeCount( + uint256 e3Id + ) external view returns (uint256) { + return committees[e3Id].activeCount; + } + + /// @inheritdoc ICiphernodeRegistry + function getCommitteeThreshold( + uint256 e3Id + ) external view returns (uint32[2] memory) { + return committees[e3Id].threshold; + } + //////////////////////////////////////////////////////////// // // // Internal Functions // @@ -521,7 +644,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Validates that a node is eligible to submit a ticket - /// @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + /// @dev Uses snapshot of ticket balance at (requestBlock - 1) for deterministic validation. + /// The -1 offset prevents same-block manipulation attacks where an operator could deposit + /// tickets and submit in the same transaction. Deposits in the request block itself are + /// excluded. This is conservative but not fully settled — see TODO below. /// @param node Address of the ciphernode /// @param ticketNumber The ticket number being submitted /// @param e3Id ID of the E3 computation @@ -554,7 +680,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Inserts a node into the top-N list - Smallest scores - /// @dev If the node is not in the top-N, it is added to the top-N. + /// @dev O(N) linear scan per insertion to find the worst score. For a committee of size N + /// with S total submissions, total gas is O(N * S). With N=20 and S=1000, this is ~20K + /// iterations at ~200 gas each (≈ 4M gas total), which is acceptable for current + /// parameters. Will not scale to N > ~50 without switching to a heap or sorted + /// data structure. /// @param c Committee storage reference /// @param node Address of the node /// @param score Score of the node diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 7001ef127d..7cbfd2c680 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -9,26 +9,32 @@ pragma solidity >=0.8.27; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; -import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; +import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { IEnclave } from "../interfaces/IEnclave.sol"; +import { ICircuitVerifier } from "../interfaces/ICircuitVerifier.sol"; /** * @title SlashingManager - * @notice Implementation of slashing management with proposal, appeal, and execution workflows - * @dev Role-based access control for slashers, verifiers, and governance with configurable slash policies + * @notice Implementation of slashing management with two-lane architecture: + * Lane A (proof-based): permissionless, atomic propose+execute, no appeals + * Lane B (evidence-based): SLASHER_ROLE required, appeal window, separate execute + * @dev Role-based access control for slashers and governance with configurable slash policies. + * Integrates with CiphernodeRegistry for committee expulsion and Enclave for E3 failure. */ contract SlashingManager is ISlashingManager, AccessControl { // ====================== // Constants & Roles // ====================== - /// @notice Role identifier for accounts authorized to propose and execute slashes + /// @notice Role identifier for accounts authorized to propose evidence-based slashes bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - /// @notice Role identifier for accounts authorized to verify cryptographic proofs in slash proposals - bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); - /// @notice Role identifier for governance accounts that can configure policies, resolve appeals, and manage bans bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); @@ -37,45 +43,54 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @notice Reference to the bonding registry contract where slash penalties are executed - /// @dev Used to call slashTicketBalance() and slashLicenseBond() when executing slashes IBondingRegistry public bondingRegistry; + /// @notice Reference to the ciphernode registry for committee expulsion + ICiphernodeRegistry public ciphernodeRegistry; + + /// @notice Reference to the Enclave contract for E3 failure signaling + IEnclave public enclave; + /// @notice Mapping from slash reason hash to its configured policy - /// @dev Stores penalty amounts, proof requirements, and appeal settings for each slash type mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; /// @notice Internal storage for all slash proposals indexed by proposal ID - /// @dev Sequentially indexed starting from 0, accessed via getSlashProposal() mapping(uint256 proposalId => SlashProposal proposal) internal _proposals; /// @notice Counter for total number of slash proposals ever created - /// @dev Also serves as the next proposal ID to be assigned uint256 public totalProposals; /// @notice Mapping tracking which nodes are currently banned from the network - /// @dev Set to true when a node is banned (either via executeSlash or banNode), false when unbanned mapping(address node => bool banned) public banned; + /// @notice Evidence replay protection: tracks consumed evidence keys + /// @dev Key is keccak256(abi.encode(e3Id, operator, keccak256(proof))) — reason-independent + /// to prevent the same proof/evidence from being used to slash under multiple reasons. + mapping(bytes32 evidenceKey => bool consumed) public evidenceConsumed; + + // ====================== + // Constants + // ====================== + + /// @notice EIP-712 style typehash for the operator's signed proof payload. + /// @dev Must match `ProofPayload::typehash()` in `crates/events/src/enclave_event/signed_proof.rs`. + /// Prevents cross-chain, cross-E3, and cross-proof-type replay of signed proofs. + bytes32 public constant PROOF_PAYLOAD_TYPEHASH = + keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)" + ); + // ====================== // Modifiers // ====================== /// @notice Restricts function access to accounts with SLASHER_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlySlasher() { if (!hasRole(SLASHER_ROLE, msg.sender)) revert Unauthorized(); _; } - /// @notice Restricts function access to accounts with VERIFIER_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role - modifier onlyVerifier() { - if (!hasRole(VERIFIER_ROLE, msg.sender)) revert Unauthorized(); - _; - } - /// @notice Restricts function access to accounts with GOVERNANCE_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlyGovernance() { if (!hasRole(GOVERNANCE_ROLE, msg.sender)) revert Unauthorized(); _; @@ -86,19 +101,26 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /** - * @notice Initializes the SlashingManager contract with admin and bonding registry - * @dev Sets up initial role assignments and bonding registry reference + * @notice Initializes the SlashingManager contract * @param admin Address to receive DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE - * @param _bondingRegistry Address of the bonding registry contract for executing slashes - * Requirements: - * - admin must not be zero address - * - _bondingRegistry must not be zero address + * @param _bondingRegistry Address of the bonding registry contract + * @param _ciphernodeRegistry Address of the ciphernode registry contract + * @param _enclave Address of the Enclave contract */ - constructor(address admin, address _bondingRegistry) { + constructor( + address admin, + address _bondingRegistry, + address _ciphernodeRegistry, + address _enclave + ) { require(admin != address(0), ZeroAddress()); require(_bondingRegistry != address(0), ZeroAddress()); + require(_ciphernodeRegistry != address(0), ZeroAddress()); + require(_enclave != address(0), ZeroAddress()); bondingRegistry = IBondingRegistry(_bondingRegistry); + ciphernodeRegistry = ICiphernodeRegistry(_ciphernodeRegistry); + enclave = IEnclave(_enclave); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(GOVERNANCE_ROLE, admin); @@ -146,7 +168,6 @@ contract SlashingManager is ISlashingManager, AccessControl { if (policy.requiresProof) { require(policy.proofVerifier != address(0), VerifierNotSet()); - // TODO: Should we allow appeal window for proof required? require(policy.appealWindow == 0, InvalidPolicy()); } else { require(policy.appealWindow > 0, InvalidPolicy()); @@ -164,6 +185,24 @@ contract SlashingManager is ISlashingManager, AccessControl { bondingRegistry = IBondingRegistry(newBondingRegistry); } + /// @notice Updates the ciphernode registry contract address + /// @param newCiphernodeRegistry Address of the new ICiphernodeRegistry contract + function setCiphernodeRegistry( + address newCiphernodeRegistry + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newCiphernodeRegistry != address(0), ZeroAddress()); + ciphernodeRegistry = ICiphernodeRegistry(newCiphernodeRegistry); + } + + /// @notice Updates the Enclave contract address + /// @param newEnclave Address of the new IEnclave contract + function setEnclave( + address newEnclave + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newEnclave != address(0), ZeroAddress()); + enclave = IEnclave(newEnclave); + } + /// @inheritdoc ISlashingManager function addSlasher(address slasher) external onlyRole(DEFAULT_ADMIN_ROLE) { require(slasher != address(0), ZeroAddress()); @@ -177,69 +216,132 @@ contract SlashingManager is ISlashingManager, AccessControl { _revokeRole(SLASHER_ROLE, slasher); } - /// @inheritdoc ISlashingManager - function addVerifier( - address verifier - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(verifier != address(0), ZeroAddress()); - _grantRole(VERIFIER_ROLE, verifier); - } - - /// @inheritdoc ISlashingManager - function removeVerifier( - address verifier - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - _revokeRole(VERIFIER_ROLE, verifier); - } - // ====================== // Slashing Functions // ====================== /// @inheritdoc ISlashingManager + /// @dev Lane A: Permissionless proof-based slash. Anyone can call. + /// Atomically proposes, verifies operator signature + ZK proof, and executes slash. + /// Evidence format: + /// `abi.encode(bytes zkProof, bytes32[] publicInputs, + /// bytes signature, + /// uint256 chainId, + /// uint256 proofType, + /// address verifier)` + /// The operator must have signed: + /// `keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, + /// chainId, + /// e3Id, + /// proofType, + /// keccak256(zkProof), + /// keccak256(abi.encodePacked(publicInputs))))` + /// This prevents: + /// - Arbitrary proof submission (attacker can't forge operator's signature) + /// - Cross-E3 replay (e3Id is in the signed message)` + /// - Cross-chain replay (chainId is in the signed message)` + /// - Verifier-upgrade attacks (verifier in evidence must match policy's current verifier)` function proposeSlash( + uint256 e3Id, address operator, bytes32 reason, bytes calldata proof - ) - external - // TODO: Do we need an onlySlasher modifier? - // Can anyone propose a slash? - onlySlasher - returns (uint256 proposalId) - { + ) external returns (uint256 proposalId) { require(operator != address(0), ZeroAddress()); SlashPolicy memory policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); + require(policy.requiresProof, InvalidPolicy()); + require(proof.length != 0, ProofRequired()); + + // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + bytes32 evidenceKey = keccak256( + abi.encode(e3Id, operator, keccak256(proof)) + ); + require(!evidenceConsumed[evidenceKey], DuplicateEvidence()); + evidenceConsumed[evidenceKey] = true; + + // Verify evidence: signature, committee membership, and ZK proof + _verifyProofEvidence(proof, e3Id, operator, policy.proofVerifier); + // Create proposal proposalId = totalProposals; totalProposals = proposalId + 1; - uint256 executableAt = block.timestamp + policy.appealWindow; SlashProposal storage p = _proposals[proposalId]; - + p.e3Id = e3Id; p.operator = operator; p.reason = reason; p.ticketAmount = policy.ticketPenalty; p.licenseAmount = policy.licensePenalty; p.proposedAt = block.timestamp; - p.executableAt = executableAt; + p.executableAt = block.timestamp; p.proposer = msg.sender; p.proofHash = keccak256(proof); + p.proofVerified = true; + // Snapshot behavioral flags from policy at proposal time + p.banNode = policy.banNode; + p.affectsCommittee = policy.affectsCommittee; + p.failureReason = policy.failureReason; - if (policy.requiresProof) { - require(proof.length != 0, ProofRequired()); - bool ok = ISlashVerifier(policy.proofVerifier).verify( - proposalId, - proof - ); - require(ok, InvalidProof()); - p.proofVerified = true; - } + emit SlashProposed( + proposalId, + e3Id, + operator, + reason, + policy.ticketPenalty, + policy.licensePenalty, + block.timestamp, + msg.sender + ); + + _executeSlash(proposalId); + } + + /// @inheritdoc ISlashingManager + /// @dev Lane B: Evidence-based slash with appeal window. SLASHER_ROLE required. + function proposeSlashEvidence( + uint256 e3Id, + address operator, + bytes32 reason, + bytes calldata evidence + ) external onlySlasher returns (uint256 proposalId) { + require(operator != address(0), ZeroAddress()); + + SlashPolicy memory policy = slashPolicies[reason]; + require(policy.enabled, SlashReasonDisabled()); + require(!policy.requiresProof, InvalidPolicy()); + + // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + bytes32 evidenceKey = keccak256( + abi.encode(e3Id, operator, keccak256(evidence)) + ); + require(!evidenceConsumed[evidenceKey], DuplicateEvidence()); + evidenceConsumed[evidenceKey] = true; + + proposalId = totalProposals; + totalProposals = proposalId + 1; + + uint256 executableAt = block.timestamp + policy.appealWindow; + SlashProposal storage p = _proposals[proposalId]; + p.e3Id = e3Id; + p.operator = operator; + p.reason = reason; + p.ticketAmount = policy.ticketPenalty; + p.licenseAmount = policy.licensePenalty; + p.proposedAt = block.timestamp; + p.executableAt = executableAt; + p.proposer = msg.sender; + p.proofHash = keccak256(evidence); + // Snapshot behavioral flags from policy at proposal time + // to prevent execution drift if policy is modified during appeal window + p.banNode = policy.banNode; + p.affectsCommittee = policy.affectsCommittee; + p.failureReason = policy.failureReason; emit SlashProposed( proposalId, + e3Id, operator, reason, policy.ticketPenalty, @@ -250,28 +352,126 @@ contract SlashingManager is ISlashingManager, AccessControl { } /// @inheritdoc ISlashingManager + /// @dev Only for evidence-based slashes (Lane B). Proof-based slashes execute atomically. function executeSlash(uint256 proposalId) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; - - // Has already been executed? require(!p.executed, AlreadyExecuted()); - p.executed = true; - SlashPolicy memory policy = slashPolicies[p.reason]; + // Use snapshotted requiresProof state: proof-based slashes are already executed atomically in proposeSlash + require(!p.proofVerified, InvalidPolicy()); - if (policy.requiresProof) { - // Appeal window is 0 by policy validation, so we dont check for appeal gating - require(p.proofVerified, InvalidProof()); - } else { - // Evidence mode with appeals - require(block.timestamp >= p.executableAt, AppealWindowActive()); - if (p.appealed) { - require(p.resolved, AppealPending()); - require(!p.appealUpheld, AppealUpheld()); // approved = appeal upheld => cancel slash, return? + // Evidence mode: check appeal window + require(block.timestamp >= p.executableAt, AppealWindowActive()); + if (p.appealed) { + require(p.resolved, AppealPending()); + require(!p.appealUpheld, AppealUpheld()); + } + + _executeSlash(proposalId); + } + + // ====================== + // Internal Execution + // ====================== + + /// @dev Verifies the operator is/was a committee member for the given E3. + function _verifyCommitteeMembership( + uint256 e3Id, + address operator + ) internal view { + address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( + e3Id + ); + bool isMember = false; + for (uint256 i = 0; i < committeeNodes.length; i++) { + if (committeeNodes[i] == operator) { + isMember = true; + break; } } + require(isMember, OperatorNotInCommittee()); + } + + /// @dev Decodes evidence, verifies operator signature, committee membership, + /// and that the ZK proof is invalid (fault confirmed). + /// Evidence format: + /// `abi.encode(bytes zkProof, bytes32[] publicInputs, + /// bytes signature, + /// uint256 chainId, + /// uint256 proofType, + /// address verifier)` + function _verifyProofEvidence( + bytes calldata proof, + uint256 e3Id, + address operator, + address policyVerifier + ) internal view { + ( + bytes memory zkProof, + bytes32[] memory publicInputs, + bytes memory signature, + uint256 chainId, + uint256 proofType, + address signedVerifier + ) = abi.decode( + proof, + (bytes, bytes32[], bytes, uint256, uint256, address) + ); + + // 1. Verify verifier in evidence matches policy's current verifier. + require(signedVerifier == policyVerifier, VerifierMismatch()); + + // 1b. Verify chainId matches current chain to prevent cross-chain replay. + require(chainId == block.chainid, ChainIdMismatch()); + + // 2. Verify the operator signed this exact proof payload. + bytes32 messageHash = keccak256( + abi.encode( + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + keccak256(zkProof), + keccak256(abi.encodePacked(publicInputs)) + ) + ); + bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash( + messageHash + ); + address recoveredSigner = ECDSA.recover(ethSignedHash, signature); + require(recoveredSigner == operator, SignerIsNotOperator()); + + // 3. Verify committee membership. + _verifyCommitteeMembership(e3Id, operator); + // 4. Re-verify the ZK proof on-chain (INVERTED: must FAIL to confirm fault). + // The staticcall MUST succeed — if the verifier reverts or doesn't exist, + // we cannot determine fault and must not slash (M-04 fix). + (bool callSuccess, bytes memory returnData) = policyVerifier.staticcall( + abi.encodeCall(ICircuitVerifier.verify, (zkProof, publicInputs)) + ); + require(callSuccess, VerifierCallFailed()); + require(returnData.length >= 32, VerifierCallFailed()); + bool proofValid = abi.decode(returnData, (bool)); + if (proofValid) revert ProofIsValid(); + } + + /** + * @notice Internal function that executes a slash and handles committee expulsion + * @dev For Lane B (delayed execution), the operator may have deregistered during the appeal + * window. BondingRegistry.slashTicketBalance and slashLicenseBond use Math.min(requested, + * available), so zero-balance operators receive a zero slash gracefully. The exit queue's + * slashPendingAssets(includeLockedAssets=true) covers operators mid-exit. If the operator + * has already claimed their exit, funds are gone and the slash amount becomes 0. This is + * an accepted tradeoff for the appeal window design. + * @param proposalId ID of the proposal to execute + */ + function _executeSlash(uint256 proposalId) internal { + SlashProposal storage p = _proposals[proposalId]; + p.executed = true; + + // Execute financial penalties if (p.ticketAmount > 0) { bondingRegistry.slashTicketBalance( p.operator, @@ -288,18 +488,32 @@ contract SlashingManager is ISlashingManager, AccessControl { ); } - if (policy.banNode) { + // Ban node if snapshotted policy requires it + if (p.banNode) { banned[p.operator] = true; - emit NodeBanUpdated(p.operator, true, p.reason, msg.sender); + emit NodeBanUpdated(p.operator, true, p.reason, address(this)); + } + + // Committee expulsion for E3-scoped slashes (uses snapshotted behavioral flags) + // expelCommitteeMember returns (activeCount, thresholdM) — one call instead of three + if (p.affectsCommittee) { + (uint256 activeCount, uint32 thresholdM) = ciphernodeRegistry + .expelCommitteeMember(p.e3Id, p.operator, p.reason); + + // If active count drops below M, fail the E3 + if (activeCount < thresholdM && p.failureReason > 0) { + try enclave.onE3Failed(p.e3Id, p.failureReason) {} catch {} + } } emit SlashExecuted( proposalId, + p.e3Id, p.operator, p.reason, p.ticketAmount, p.licenseAmount, - p.executed + true ); } @@ -308,17 +522,22 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @inheritdoc ISlashingManager + /// @dev Only the accused operator can file an appeal. No delegate, multi-sig, or representative + /// patterns exist. If the operator has lost access to their key or been banned, they cannot + /// appeal. Consider adding an appealDelegate mapping for production to allow a designated + /// representative to appeal on behalf of the operator. function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); - // TODO: Should we reject the appeal if the proposal has a cryptographic proof? SlashProposal storage p = _proposals[proposalId]; // Only the accused can appeal require(msg.sender == p.operator, Unauthorized()); - // Only in the window + // Only within the appeal window require(block.timestamp < p.executableAt, AppealWindowExpired()); // Only once require(!p.appealed, AlreadyAppealed()); + // Cannot appeal proof-verified slashes (they have no appeal window) + require(!p.proofVerified, InvalidProposal()); p.appealed = true; @@ -338,7 +557,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(!p.resolved, AlreadyResolved()); p.resolved = true; - p.appealUpheld = appealUpheld; // true => cancel slash, false => slash stands + p.appealUpheld = appealUpheld; emit AppealResolved( proposalId, diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index db92a079df..a16cdc38cf 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -10,6 +10,20 @@ import { IEnclave } from "../interfaces/IEnclave.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { + /// @notice Configurable committee members per E3 for testing + mapping(uint256 e3Id => address[] nodes) private _committeeNodes; + + /// @notice Set committee members for an E3 (test helper) + function setCommitteeNodes( + uint256 e3Id, + address[] calldata nodes + ) external { + delete _committeeNodes[e3Id]; + for (uint256 i = 0; i < nodes.length; i++) { + _committeeNodes[e3Id].push(nodes[i]); + } + } + function requestCommittee( uint256, uint256, @@ -52,10 +66,9 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { ) external pure {} // solhint-disable-line no-empty-blocks function getCommitteeNodes( - uint256 - ) external pure returns (address[] memory) { - address[] memory nodes = new address[](0); - return nodes; + uint256 e3Id + ) external view returns (address[] memory) { + return _committeeNodes[e3Id]; } function root() external pure returns (uint256) { @@ -94,6 +107,38 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function isOpen(uint256) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function expelCommitteeMember( + uint256, + address, + bytes32 + ) external pure returns (uint256, uint32) { + return (0, 0); + } + + function isCommitteeMemberActive( + uint256, + address + ) external pure returns (bool) { + return true; + } + + function getActiveCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + return new address[](0); + } + + function getActiveCommitteeCount(uint256) external pure returns (uint256) { + return 0; + } + + function getCommitteeThreshold( + uint256 + ) external pure returns (uint32[2] memory) { + return [uint32(0), uint32(0)]; + } } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { @@ -179,4 +224,36 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function isOpen(uint256) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function expelCommitteeMember( + uint256, + address, + bytes32 + ) external pure returns (uint256, uint32) { + return (0, 0); + } + + function isCommitteeMemberActive( + uint256, + address + ) external pure returns (bool) { + return true; + } + + function getActiveCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + return new address[](0); + } + + function getActiveCommitteeCount(uint256) external pure returns (uint256) { + return 0; + } + + function getCommitteeThreshold( + uint256 + ) external pure returns (uint32[2] memory) { + return [uint32(0), uint32(0)]; + } } diff --git a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol index 9a134afbea..5097d5eca8 100644 --- a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol +++ b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol @@ -5,15 +5,22 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; -import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; +import { ICircuitVerifier } from "../interfaces/ICircuitVerifier.sol"; -contract MockSlashingVerifier is ISlashVerifier { - function verify( - uint256, - bytes memory data - ) external pure returns (bool success) { - data; +/// @notice Mock circuit verifier for testing. Returns configurable result. +/// @dev Default returnValue = false means proof is invalid = fault confirmed (slash proceeds). +/// Set returnValue = true to simulate a valid proof = no fault (ProofIsValid revert). +contract MockCircuitVerifier is ICircuitVerifier { + bool public returnValue; + + function setReturnValue(bool _returnValue) external { + returnValue = _returnValue; + } - if (data.length > 0) success = true; + function verify( + bytes calldata, + bytes32[] calldata + ) external view returns (bool) { + return returnValue; } } diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol index 9d932d6a66..b7be111ef8 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -57,6 +57,10 @@ contract EnclaveTicketToken is /// @dev Only this contract can call restricted functions like depositFor, withdrawTo, burnTickets, and payout address public registry; + /// @notice Tracks slashed funds available for payout (L-12 defense-in-depth) + /// @dev Incremented by burnTickets, decremented by payout. Prevents payout exceeding slashed amount. + uint256 public payableBalance; + /// @notice Restricts function access to only the registry contract /// @dev Reverts with NotRegistry if caller is not the registry address modifier onlyRegistry() { @@ -101,6 +105,7 @@ contract EnclaveTicketToken is * @dev Only callable by the registry contract. Transfers underlying tokens from the registry to * this contract and mints an equivalent amount of ticket tokens. Automatically delegates * voting power to the operator on their first deposit to enable voting power tracking. + * Combined with delegate() reverting DelegationLocked(), operators permanently self-delegate. * @param operator Address to receive the minted ticket tokens * @param amount Number of underlying tokens to deposit and ticket tokens to mint * @return success True if the deposit and minting succeeded @@ -172,6 +177,7 @@ contract EnclaveTicketToken is address operator, uint256 amount ) external onlyRegistry { + payableBalance += amount; _burn(operator, amount); } @@ -182,9 +188,18 @@ contract EnclaveTicketToken is * @param amount Amount of ticket tokens to payout. */ function payout(address to, uint256 amount) external onlyRegistry { + require(amount <= payableBalance, "Exceeds payable balance"); + payableBalance -= amount; SafeERC20.safeTransfer(IERC20(address(underlying())), to, amount); } + /** + * @dev Override approve to revert — ticket tokens are non-transferable. + */ + function approve(address, uint256) public pure override returns (bool) { + revert TransferNotAllowed(); + } + /** * @dev Override ERC20Votes update hook to prevent transfers between users. */ diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index f1e5534008..0443d2d218 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -60,7 +60,6 @@ contract EnclaveToken is bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); /// @notice Tracks the cumulative amount of tokens minted since deployment - /// @dev Incremented with each mint operation to enforce MAX_SUPPLY cap uint256 public totalMinted; /// @notice Mapping of addresses permitted to transfer tokens when restrictions are active @@ -158,6 +157,7 @@ contract EnclaveToken is for (uint256 i = 0; i < len; i++) { address recipient = recipients[i]; uint256 amount = amounts[i]; + if (recipient == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); if (amount > MAX_SUPPLY - minted) revert ExceedsTotalSupply(); @@ -222,6 +222,7 @@ contract EnclaveToken is * @dev Overrides ERC20 and ERC20Votes to add transfer restriction logic. Reverts if transfers * are restricted and neither sender nor receiver is whitelisted. Minting (from == 0) and * burning (to == 0) are always allowed regardless of restrictions. + * * @param from Address sending tokens (zero address for minting) * @param to Address receiving tokens (zero address for burning) * @param value Amount of tokens being transferred diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 703194cdfb..2e5973e418 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -132,5 +132,141 @@ "blockNumber": 10279540, "address": "0xB886C067e9C1D2B31461F4DFd29f557B5714297d" } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001", + "ciphernodeRegistry": "0x0000000000000000000000000000000000000001", + "enclave": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "proxyRecords": { + "initData": "0x7333fa82000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000093a80", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "proxyAdminAddress": "0x94099942864EA81cCF197E9D71ac53310b1468D8", + "implementationAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "blockNumber": 8, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "10" + }, + "proxyRecords": { + "initData": "0x1794bb3c000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "proxyAdminAddress": "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + "implementationAddress": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "blockNumber": 11, + "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000" + ] + }, + "proxyRecords": { + "initData": "0x69c5b347000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", + "implementationAddress": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "blockNumber": 13, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockComputeProvider": { + "blockNumber": 29, + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + }, + "MockDecryptionVerifier": { + "blockNumber": 30, + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" + }, + "MockE3Program": { + "blockNumber": 31, + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + }, + "ZKTranscriptLib": { + "blockNumber": 34, + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" + }, + "DkgPkVerifier": { + "blockNumber": 35, + "address": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042" + } } } \ No newline at end of file diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index 9f216e7544..e6990e143f 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -18,7 +18,6 @@ export default buildModule("Enclave", (m) => { dkgWindow: 7200, computeWindow: 86400, decryptionWindow: 3600, - gracePeriod: 600, }); const enclaveImpl = m.contract("Enclave", []); diff --git a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts index 3c4b663b6a..17a6ce70d6 100644 --- a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts +++ b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts @@ -5,8 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -export default buildModule("MockSlashingVerifier", (m) => { - const mockSlashingVerifier = m.contract("MockSlashingVerifier"); +export default buildModule("MockCircuitVerifier", (m) => { + const mockCircuitVerifier = m.contract("MockCircuitVerifier"); - return { mockSlashingVerifier }; + return { mockCircuitVerifier }; }) as any; diff --git a/packages/enclave-contracts/ignition/modules/slashingManager.ts b/packages/enclave-contracts/ignition/modules/slashingManager.ts index 0d5919900e..e44ad80287 100644 --- a/packages/enclave-contracts/ignition/modules/slashingManager.ts +++ b/packages/enclave-contracts/ignition/modules/slashingManager.ts @@ -3,17 +3,19 @@ // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. - -/* eslint-disable @typescript-eslint/no-explicit-any */ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("SlashingManager", (m) => { const bondingRegistry = m.getParameter("bondingRegistry"); + const ciphernodeRegistry = m.getParameter("ciphernodeRegistry"); + const enclave = m.getParameter("enclave"); const admin = m.getParameter("admin"); const slashingManager = m.contract("SlashingManager", [ admin, bondingRegistry, + ciphernodeRegistry, + enclave, ]); return { slashingManager }; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index ff60f4217c..b8aa4f0dfd 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -21,7 +21,6 @@ export interface E3TimeoutConfig { dkgWindow: number; computeWindow: number; decryptionWindow: number; - gracePeriod: number; } /** diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts index 47b7e179e6..fdd28469b8 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -17,6 +17,8 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; export interface SlashingManagerArgs { admin?: string; bondingRegistry?: string; + ciphernodeRegistry?: string; + enclave?: string; hre: HardhatRuntimeEnvironment; } @@ -28,6 +30,8 @@ export interface SlashingManagerArgs { export const deployAndSaveSlashingManager = async ({ admin, bondingRegistry, + ciphernodeRegistry, + enclave, hre, }: SlashingManagerArgs): Promise<{ slashingManager: SlashingManager; @@ -41,8 +45,13 @@ export const deployAndSaveSlashingManager = async ({ if ( !admin || !bondingRegistry || + !ciphernodeRegistry || + !enclave || (preDeployedArgs?.constructorArgs?.admin === admin && - preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry) + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.ciphernodeRegistry === + ciphernodeRegistry && + preDeployedArgs?.constructorArgs?.enclave === enclave) ) { if (!preDeployedArgs?.address) { throw new Error( @@ -61,6 +70,8 @@ export const deployAndSaveSlashingManager = async ({ const slashingManager = await slashingManagerFactory.deploy( admin, bondingRegistry, + ciphernodeRegistry, + enclave, ); await slashingManager.waitForDeployment(); @@ -74,6 +85,8 @@ export const deployAndSaveSlashingManager = async ({ constructorArgs: { admin, bondingRegistry, + ciphernodeRegistry, + enclave, }, blockNumber, address: slashingManagerAddress, diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 5c9670f0be..8da5a2668d 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -26,7 +26,6 @@ const DEFAULT_TIMEOUT_CONFIG = { dkgWindow: 7200, computeWindow: 86400, decryptionWindow: 3600, - gracePeriod: 600, }; /** @@ -102,6 +101,8 @@ export const deployEnclave = async (withMocks?: boolean) => { const { slashingManager } = await deployAndSaveSlashingManager({ admin: ownerAddress, bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, hre, }); const slashingManagerAddress = await slashingManager.getAddress(); @@ -189,9 +190,18 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting BondingRegistry address in SlashingManager..."); await slashingManager.setBondingRegistry(bondingRegistryAddress); + console.log("Setting CiphernodeRegistry address in SlashingManager..."); + await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); + + console.log("Setting Enclave address in SlashingManager..."); + await slashingManager.setEnclave(enclaveAddress); + console.log("Setting SlashingManager address in BondingRegistry..."); await bondingRegistry.setSlashingManager(slashingManagerAddress); + console.log("Setting SlashingManager address in CiphernodeRegistry..."); + await ciphernodeRegistry.setSlashingManager(slashingManagerAddress); + console.log("Setting Enclave as reward distributor in BondingRegistry..."); await bondingRegistry.setRewardDistributor(enclaveAddress); diff --git a/packages/enclave-contracts/scripts/utils.ts b/packages/enclave-contracts/scripts/utils.ts index a86684fe5d..53ca1ab1d4 100644 --- a/packages/enclave-contracts/scripts/utils.ts +++ b/packages/enclave-contracts/scripts/utils.ts @@ -39,6 +39,7 @@ export interface EnclaveConfig { enclave?: { address: string; deploy_block: number }; ciphernode_registry?: { address: string; deploy_block: number }; bonding_registry?: { address: string; deploy_block: number }; + slashing_manager?: { address: string; deploy_block: number }; fee_token?: { address: string; deploy_block: number }; }; }>; diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index 6b44dc185f..e42635d1cb 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -56,7 +56,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { dkgWindow: ONE_DAY, computeWindow: THREE_DAYS, decryptionWindow: ONE_DAY, - gracePeriod: ONE_HOUR, }; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); @@ -128,6 +127,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: addressOne, // Will be updated + ciphernodeRegistry: addressOne, // Will be updated + enclave: addressOne, // Will be updated }, }, }, @@ -253,7 +254,14 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await slashingManagerContract.slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), ); + await slashingManagerContract.slashingManager.setCiphernodeRegistry( + ciphernodeRegistryAddress, + ); + await slashingManagerContract.slashingManager.setEnclave(enclaveAddress); await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); // Update ticket token registry await ticketTokenContract.enclaveTicketToken.setRegistry( diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 4b6ac074e5..54969c83e6 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -43,7 +43,6 @@ describe("Enclave", function () { dkgWindow: 3600, // 1 hour computeWindow: 3600, // 1 hour decryptionWindow: 3600, // 1 hour - gracePeriod: 300, // 5 minutes }; const inputWindowDuration = 300; @@ -185,6 +184,8 @@ describe("Enclave", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, }, }, }, @@ -931,11 +932,8 @@ describe("Enclave", function () { await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) - .to.be.revertedWithCustomError( - enclave, - "CiphertextOutputAlreadyPublished", - ) - .withArgs(e3Id); + .to.be.revertedWithCustomError(enclave, "InvalidStage") + .withArgs(e3Id, 3, 4); }); it("reverts if committee duties are over", async function () { const { diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index b53d785db1..479f1e7269 100644 --- a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -97,6 +97,8 @@ describe("BondingRegistry", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: AddressOne, + ciphernodeRegistry: AddressOne, + enclave: AddressOne, }, }, }, diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 9a2db8f877..570395f84f 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -134,6 +134,8 @@ describe("CiphernodeRegistryOwnable", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: AddressOne, + ciphernodeRegistry: AddressOne, + enclave: AddressOne, }, }, }, @@ -175,7 +177,6 @@ describe("CiphernodeRegistryOwnable", function () { dkgWindow: 3600, computeWindow: 3600, decryptionWindow: 3600, - gracePeriod: 300, }, }, }, diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts new file mode 100644 index 0000000000..ba1918e6fd --- /dev/null +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -0,0 +1,957 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/** + * Tests for committee expulsion, viability checks, and E3 failure on threshold breach. + * + * Verifies: + * - Committee members are expelled via proposeSlash when affectsCommittee=true + * - The E3 continues as long as active members >= threshold M + * - The E3 fails when active members drop below threshold M + * - Rewards exclude expelled members + * - Idempotent expulsion (re-slashing same node doesn't double-count) + */ +import { expect } from "chai"; +import type { Signer } from "ethers"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import EnclaveModule from "../../ignition/modules/enclave"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockDecryptionVerifierModule from "../../ignition/modules/mockDecryptionVerifier"; +import MockE3ProgramModule from "../../ignition/modules/mockE3Program"; +import MockCircuitVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, + Enclave__factory as EnclaveFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockCircuitVerifier__factory as MockCircuitVerifierFactory, + MockDecryptionVerifier__factory as MockDecryptionVerifierFactory, + MockE3Program__factory as MockE3ProgramFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; + +const { ethers, ignition, networkHelpers } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +describe("Committee Expulsion & Fault Tolerance", function () { + const ONE_HOUR = 60 * 60; + const ONE_DAY = 24 * ONE_HOUR; + const THREE_DAYS = 3 * ONE_DAY; + const SEVEN_DAYS = 7 * ONE_DAY; + const THIRTY_DAYS = 30 * ONE_DAY; + const SORTITION_SUBMISSION_WINDOW = 10; + const addressOne = "0x0000000000000000000000000000000000000001"; + + const REASON_BAD_DKG = ethers.keccak256( + ethers.toUtf8Bytes("E3_BAD_DKG_PROOF"), + ); + const REASON_BAD_DECRYPTION = ethers.keccak256( + ethers.toUtf8Bytes("E3_BAD_DECRYPTION_PROOF"), + ); + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const polynomial_degree = ethers.toBigInt(2048); + const plaintext_modulus = ethers.toBigInt(1032193); + const moduli = [ethers.toBigInt("18014398492704769")]; + + const encodedE3ProgramParams = abiCoder.encode( + ["uint256", "uint256", "uint256[]"], + [polynomial_degree, plaintext_modulus, moduli], + ); + + const encryptionSchemeId = + "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; + + const defaultTimeoutConfig = { + dkgWindow: ONE_DAY, + computeWindow: THREE_DAYS, + decryptionWindow: ONE_DAY, + }; + + // Must match the PROOF_PAYLOAD_TYPEHASH in SlashingManager.sol + const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ), + ); + + /** + * Helper to create a signed proof evidence bundle. + * The operator signs the proof payload (matching SlashingManager._verifyProofEvidence), + * then the evidence is encoded in the 6-field format expected by proposeSlash(). + */ + async function signAndEncodeProof( + signer: Signer, + e3Id: number, + verifierAddress: string, + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + chainId: number = 31337, + proofType: number = 0, + ): Promise { + const messageHash = ethers.keccak256( + abiCoder.encode( + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32"], + [ + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + ethers.keccak256(zkProof), + ethers.keccak256( + ethers.solidityPacked(["bytes32[]"], [publicInputs]), + ), + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], + ); + } + + const setup = async () => { + const [ + owner, + requester, + treasury, + operator1, + operator2, + operator3, + operator4, + ] = await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + const treasuryAddress = await treasury.getAddress(); + const requesterAddress = await requester.getAddress(); + + // Deploy tokens + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 10000000 } }, + }); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { EnclaveToken: { owner: ownerAddress } }, + }); + const enclToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + await enclToken.setTransferRestriction(false); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcToken.getAddress(), + registry: addressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const mockVerifierContract = await ignition.deploy( + MockCircuitVerifierModule, + ); + const mockVerifier = MockCircuitVerifierFactory.connect( + await mockVerifierContract.mockCircuitVerifier.getAddress(), + owner, + ); + + // Deploy slashing manager + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, + }, + }, + }, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + + // Deploy bonding registry + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclToken.getAddress(), + registry: addressOne, + slashedFundsTreasury: treasuryAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 1, + exitDelay: SEVEN_DAYS, + }, + }, + }, + ); + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + + // Deploy Enclave + const enclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: ownerAddress, + maxDuration: THIRTY_DAYS, + registry: addressOne, + e3RefundManager: addressOne, + bondingRegistry: await bondingRegistry.getAddress(), + feeToken: await usdcToken.getAddress(), + timeoutConfig: defaultTimeoutConfig, + }, + }, + }); + const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclave = EnclaveFactory.connect(enclaveAddress, owner); + + // Deploy CiphernodeRegistry + const ciphernodeRegistryContract = await ignition.deploy( + CiphernodeRegistryModule, + { + parameters: { + CiphernodeRegistry: { + enclaveAddress: enclaveAddress, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }, + ); + const registryAddress = + await ciphernodeRegistryContract.cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryOwnableFactory.connect( + registryAddress, + owner, + ); + + // Deploy mock E3 program + const e3ProgramContract = await ignition.deploy(MockE3ProgramModule, { + parameters: { + MockE3Program: { + encryptionSchemeId: encryptionSchemeId, + }, + }, + }); + const e3Program = MockE3ProgramFactory.connect( + await e3ProgramContract.mockE3Program.getAddress(), + owner, + ); + + // Deploy mock decryption verifier + const decryptionVerifierContract = await ignition.deploy( + MockDecryptionVerifierModule, + ); + const decryptionVerifier = MockDecryptionVerifierFactory.connect( + await decryptionVerifierContract.mockDecryptionVerifier.getAddress(), + owner, + ); + + // Wire everything together + await enclave.setCiphernodeRegistry(registryAddress); + await enclave.enableE3Program(await e3Program.getAddress()); + await enclave.setDecryptionVerifier( + encryptionSchemeId, + await decryptionVerifier.getAddress(), + ); + await enclave.setSlashingManager(await slashingManager.getAddress()); + + await bondingRegistry.setRewardDistributor(enclaveAddress); + await bondingRegistry.setRegistry(registryAddress); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await slashingManager.setCiphernodeRegistry(registryAddress); + await slashingManager.setEnclave(enclaveAddress); + + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setSlashingManager(await slashingManager.getAddress()); + + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistry.getAddress(), + ); + + // Mint tokens to requester for E3 requests + await usdcToken.mint(requesterAddress, ethers.parseUnits("100000", 6)); + + // Helper: setup an operator (bond license, register, add tickets) + async function setupOperator(operator: Signer) { + const operatorAddress = await operator.getAddress(); + await enclToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await enclToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketTokenAddress = await bondingRegistry.ticketToken(); + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(ticketTokenAddress, ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + } + + // Helper: make an E3 request + async function makeRequest(threshold: [number, number] = [2, 3]) { + const startTime = (await time.latest()) + 100; + const requestParams = { + threshold: threshold, + inputWindow: [startTime + 100, startTime + ONE_DAY] as [number, number], + e3Program: await e3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + computeProviderParams: abiCoder.encode( + ["address"], + [await decryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + + const fee = await enclave.getE3Quote(requestParams); + await usdcToken.connect(requester).approve(enclaveAddress, fee); + await enclave.connect(requester).request(requestParams); + } + + // Helper: finalize a committee after sortition + async function finalizeCommitteeWithOperators( + e3Id: number, + operators: Signer[], + ) { + for (const op of operators) { + await registry.connect(op).submitTicket(e3Id, 1); + } + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + + // Publish the committee key so getCommitteeNodes works + const nodes = await Promise.all(operators.map((op) => op.getAddress())); + const publicKey = ethers.toUtf8Bytes("fake-public-key"); + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(e3Id, nodes, publicKey, publicKeyHash); + } + + // Set up committee-affecting slash policy + // MockCircuitVerifier returns false by default → proof invalid → fault confirmed + const committeeSlashPolicy = { + ticketPenalty: ethers.parseUnits("10", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 4, // FailureReason.DKGInvalidShares + }; + await slashingManager.setSlashPolicy(REASON_BAD_DKG, committeeSlashPolicy); + + const decryptionSlashPolicy = { + ticketPenalty: ethers.parseUnits("10", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 11, // FailureReason.DecryptionInvalidShares + }; + await slashingManager.setSlashPolicy( + REASON_BAD_DECRYPTION, + decryptionSlashPolicy, + ); + + return { + enclave, + registry, + slashingManager, + bondingRegistry, + mockVerifier, + usdcToken, + enclToken, + owner, + requester, + treasury, + operator1, + operator2, + operator3, + operator4, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + }; + }; + + describe("committee expulsion via proposeSlash", function () { + it("should expel a committee member and emit CommitteeMemberExpelled", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + // threshold [2, 3] means M=2, N=3 + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + const op1Address = await operator1.getAddress(); + + // Verify member is active before slash + expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.true; + expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + + // Submit slash proposal — MockCircuitVerifier returns false by default + // so fault is confirmed and slash is auto-executed + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const tx = await slashingManager.proposeSlash( + 0, + op1Address, + REASON_BAD_DKG, + proof, + ); + + // Should emit CommitteeMemberExpelled + await expect(tx) + .to.emit(registry, "CommitteeMemberExpelled") + .withArgs(0, op1Address, REASON_BAD_DKG, 2); + + // Should emit CommitteeViabilityUpdated + await expect(tx) + .to.emit(registry, "CommitteeViabilityUpdated") + .withArgs(0, 2, 2, true); // 2 >= 2 → viable + + // Verify member is no longer active + expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.false; + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + }); + + it("should keep E3 alive when active members >= threshold", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash one member — 3 active → 2 active, threshold is 2, still viable + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + // E3 should NOT be failed — stage should still be Requested (1) + // or whatever stage it was at, not Failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); // 6 = E3Stage.Failed + + // Active committee still has enough members + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + const threshold = await registry.getCommitteeThreshold(0); + expect(threshold[0]).to.equal(2); // M=2 + }); + + it("should fail E3 when active members drop below threshold", async function () { + const { + enclave, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash first member — 3 → 2 active, still >= 2 + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + + let stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); // Not failed yet + + // Slash second member — 2 → 1 active, below threshold M=2 + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + const tx = await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // Should emit E3Failed event + await expect(tx).to.emit(enclave, "E3Failed"); + + // E3 should now be Failed + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // E3Stage.Failed + + // Failure reason should be DKGInvalidShares (4) + const reason = await enclave.getFailureReason(0); + expect(reason).to.equal(4); + }); + + it("should handle idempotent expulsion (re-slashing same node)", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash operator1 once + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0xaaaa", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + + // Slash operator1 again with different proof (different evidence key) + const proof2 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0xbbbb", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // Active count should still be 2 (idempotent expulsion) + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + }); + + it("should exclude expelled members from getActiveCommitteeNodes", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Before expulsion: all 3 should be in active nodes + const nodesBefore = await registry.getActiveCommitteeNodes(0); + expect(nodesBefore.length).to.equal(3); + expect(nodesBefore).to.include(await operator1.getAddress()); + + // Expel operator1 + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + // After expulsion: only 2 should be active + const nodesAfter = await registry.getActiveCommitteeNodes(0); + expect(nodesAfter.length).to.equal(2); + expect(nodesAfter).to.not.include(await operator1.getAddress()); + expect(nodesAfter).to.include(await operator2.getAddress()); + expect(nodesAfter).to.include(await operator3.getAddress()); + }); + }); + + describe("E3 continues above threshold", function () { + it("should allow multiple expulsions while staying above threshold", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + operator4, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await setupOperator(operator4); + + await makeRequest([2, 4]); // M=2, N=4 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + operator4, + ]); + + expect(await registry.getActiveCommitteeCount(0)).to.equal(4); + + // Expel 2 out of 4 — still have 2 >= M=2 + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + + // E3 should NOT be failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); + }); + }); + + describe("E3 fails below threshold", function () { + it("should fail E3 exactly at the threshold breach", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest([2, 2]); // M=2, N=2 — no room for error + await finalizeCommitteeWithOperators(0, [operator1, operator2]); + + // Expel one member: 2 → 1 < M=2 → E3 fails immediately + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const tx = await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + await expect(tx).to.emit(enclave, "E3Failed"); + + // Should emit CommitteeViabilityUpdated(viable=false) + await expect(tx) + .to.emit(registry, "CommitteeViabilityUpdated") + .withArgs(0, 1, 2, false); + + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + }); + + it("should not fail E3 twice on multiple sub-threshold expulsions", async function () { + const { + enclave, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Expel operator1 — still viable (2 >= 2) + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + + // Expel operator2 — now below threshold (1 < 2), E3 fails + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // E3 is now Failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); + + // Try to expel operator3 — E3 already failed, but onE3Failed is wrapped + // in try-catch so financial penalties are still applied + const proof3 = await signAndEncodeProof( + operator3, + 0, + await mockVerifier.getAddress(), + "0x3333", + ); + + // The third slash should succeed — penalties are applied even though E3 is already Failed. + // The onE3Failed call silently fails (try-catch) since E3 is already in Failed state. + await expect( + slashingManager.proposeSlash( + 0, + await operator3.getAddress(), + REASON_BAD_DKG, + proof3, + ), + ).to.emit(slashingManager, "SlashExecuted"); + + // E3 stage should still be Failed + const stageAfter = await enclave.getE3Stage(0); + expect(stageAfter).to.equal(6); + }); + }); + + describe("slash execution events", function () { + it("should emit SlashExecuted on proof-based committee slash", async function () { + const { + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const op1Addr = await operator1.getAddress(); + const tx = await slashingManager.proposeSlash( + 0, + op1Addr, + REASON_BAD_DKG, + proof, + ); + + await expect(tx).to.emit(slashingManager, "SlashExecuted").withArgs( + 0, // proposalId + 0, // e3Id + op1Addr, + REASON_BAD_DKG, + ethers.parseUnits("10", 6), // ticketPenalty + ethers.parseEther("50"), // licensePenalty + true, // executed + ); + }); + }); +}); diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 20b29055e1..f96574724a 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -9,19 +9,21 @@ import { network } from "hardhat"; import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; -import MockSlashingVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockCiphernodeRegistryModule from "../../ignition/modules/mockCiphernodeRegistry"; +import MockCircuitVerifierModule from "../../ignition/modules/mockSlashingVerifier"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { BondingRegistry__factory as BondingRegistryFactory, EnclaveTicketToken__factory as EnclaveTicketTokenFactory, EnclaveToken__factory as EnclaveTokenFactory, - MockSlashingVerifier__factory as MockSlashingVerifierFactory, + MockCiphernodeRegistry__factory as MockCiphernodeRegistryFactory, + MockCircuitVerifier__factory as MockCircuitVerifierFactory, MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, } from "../../types"; +import type { MockCircuitVerifier } from "../../types"; import type { SlashingManager } from "../../types/contracts/slashing/SlashingManager"; -import type { MockSlashingVerifier } from "../../types/contracts/test/MockSlashingVerifier"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture, time } = networkHelpers; @@ -32,7 +34,6 @@ describe("SlashingManager", function () { const REASON_DOUBLE_SIGN = ethers.encodeBytes32String("doubleSign"); const SLASHER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SLASHER_ROLE")); - const VERIFIER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("VERIFIER_ROLE")); const GOVERNANCE_ROLE = ethers.keccak256( ethers.toUtf8Bytes("GOVERNANCE_ROLE"), ); @@ -40,9 +41,76 @@ describe("SlashingManager", function () { const APPEAL_WINDOW = 7 * 24 * 60 * 60; + // Placeholder address for contracts not under test + const addressOne = "0x0000000000000000000000000000000000000001"; + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + + // Must match the PROOF_PAYLOAD_TYPEHASH in SlashingManager.sol + const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ), + ); + + /** + * Helper to create a signed proof evidence bundle. + * The operator signs the proof payload (matching Rust ProofPayload.digest()), + * then the evidence is encoded in the format expected by proposeSlash(). + * Returns abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) + */ + async function signAndEncodeProof( + signer: any, + e3Id: number, + reason: string, + verifierAddress: string, + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + chainId: number = 31337, // Hardhat default chain ID + proofType: number = 0, // T0PkBfv + ): Promise { + // Operator signs: keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, chainId, e3Id, proofType, keccak256(zkProof), keccak256(publicSignals))) + const messageHash = ethers.keccak256( + abiCoder.encode( + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32"], + [ + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + ethers.keccak256(zkProof), + ethers.keccak256( + ethers.solidityPacked(["bytes32[]"], [publicInputs]), + ), + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + // Evidence format: abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], + ); + } + + /** + * Legacy helper for tests that check early failures (before abi.decode). + * This encodes a minimal 6-tuple with dummy values for basic validation tests. + */ + function encodeDummyProof( + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + verifierAddress: string = ethers.ZeroAddress, + ): string { + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, "0x00", 31337, 0, verifierAddress], + ); + } + async function setupPolicies( slashingManager: SlashingManager, - mockVerifier: MockSlashingVerifier, + mockVerifier: MockCircuitVerifier, ) { const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), @@ -52,6 +120,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; const evidencePolicy = { @@ -62,6 +132,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; const banPolicy = { @@ -72,6 +144,8 @@ describe("SlashingManager", function () { banNode: true, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); @@ -80,7 +154,7 @@ describe("SlashingManager", function () { } async function setup() { - const [owner, slasher, verifier, operator, notTheOwner] = + const [owner, slasher, proposer, operator, notTheOwner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); const operatorAddress = await operator.getAddress(); @@ -115,9 +189,16 @@ describe("SlashingManager", function () { ); const mockVerifierContract = await ignition.deploy( - MockSlashingVerifierModule, + MockCircuitVerifierModule, + ); + + const mockCiphernodeRegistryContract = await ignition.deploy( + MockCiphernodeRegistryModule, ); + const mockCiphernodeRegistryAddress = + await mockCiphernodeRegistryContract.mockCiphernodeRegistry.getAddress(); + const slashingManagerContract = await ignition.deploy( SlashingManagerModule, { @@ -125,6 +206,8 @@ describe("SlashingManager", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: ownerAddress, + ciphernodeRegistry: mockCiphernodeRegistryAddress, + enclave: addressOne, }, }, }, @@ -162,8 +245,12 @@ describe("SlashingManager", function () { await ticketTokenContract.enclaveTicketToken.getAddress(), owner, ); - const mockVerifier = MockSlashingVerifierFactory.connect( - await mockVerifierContract.mockSlashingVerifier.getAddress(), + const mockVerifier = MockCircuitVerifierFactory.connect( + await mockVerifierContract.mockCircuitVerifier.getAddress(), + owner, + ); + const mockCiphernodeRegistry = MockCiphernodeRegistryFactory.connect( + mockCiphernodeRegistryAddress, owner, ); const slashingManager = SlashingManagerFactory.connect( @@ -192,12 +279,11 @@ describe("SlashingManager", function () { ); await slashingManager.addSlasher(await slasher.getAddress()); - await slashingManager.addVerifier(await verifier.getAddress()); return { owner, slasher, - verifier, + proposer, operator, operatorAddress, notTheOwner, @@ -207,6 +293,7 @@ describe("SlashingManager", function () { ticketToken, usdcToken, mockVerifier, + mockCiphernodeRegistry, }; } @@ -243,6 +330,8 @@ describe("SlashingManager", function () { SlashingManager: { admin: ethers.ZeroAddress, bondingRegistry: ethers.ZeroAddress, + ciphernodeRegistry: ethers.ZeroAddress, + enclave: ethers.ZeroAddress, }, }, }), @@ -251,7 +340,7 @@ describe("SlashingManager", function () { }); describe("setSlashPolicy()", function () { - it("should set a valid slash policy", async function () { + it("should set a valid proof-based slash policy", async function () { const { slashingManager, mockVerifier } = await loadFixture(setup); const policy = { @@ -262,6 +351,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect(slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy)) @@ -276,7 +367,7 @@ describe("SlashingManager", function () { expect(storedPolicy.enabled).to.equal(policy.enabled); }); - it("should set a policy without proof requirement", async function () { + it("should set an evidence-based policy (no proof required)", async function () { const { slashingManager } = await loadFixture(setup); const policy = { @@ -287,6 +378,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect(slashingManager.setSlashPolicy(REASON_INACTIVITY, policy)) @@ -305,6 +398,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -328,6 +423,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -346,6 +443,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: false, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -364,6 +463,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -382,6 +483,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -400,6 +503,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -418,6 +523,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -447,26 +554,6 @@ describe("SlashingManager", function () { ).to.be.false; }); - it("should add and remove verifier role", async function () { - const { slashingManager, notTheOwner } = await loadFixture(setup); - - await slashingManager.addVerifier(await notTheOwner.getAddress()); - expect( - await slashingManager.hasRole( - VERIFIER_ROLE, - await notTheOwner.getAddress(), - ), - ).to.be.true; - - await slashingManager.removeVerifier(await notTheOwner.getAddress()); - expect( - await slashingManager.hasRole( - VERIFIER_ROLE, - await notTheOwner.getAddress(), - ), - ).to.be.false; - }); - it("should revert if non-admin tries to add slasher", async function () { const { slashingManager, notTheOwner } = await loadFixture(setup); @@ -474,7 +561,10 @@ describe("SlashingManager", function () { slashingManager .connect(notTheOwner) .addSlasher(await notTheOwner.getAddress()), - ).to.be.revert(ethers); + ).to.be.revertedWithCustomError( + slashingManager, + "AccessControlUnauthorizedAccount", + ); }); it("should revert if zero address is added as slasher", async function () { @@ -486,129 +576,213 @@ describe("SlashingManager", function () { }); }); - describe("proposeSlash()", function () { - it("should propose slash with proof", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + describe("proposeSlash() — Lane A (proof-based, permissionless)", function () { + it("should propose and auto-execute slash with signed proof from operator", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + // MockCircuitVerifier default returnValue=false → proof invalid → fault confirmed + const verifierAddress = await mockVerifier.getAddress(); const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: verifierAddress, banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const proof = ethers.toUtf8Bytes("Valid proof data"); - const currentTime = await time.latest(); + // Set up committee membership for operator + const e3Id = 0; + await mockCiphernodeRegistry.setCommitteeNodes(e3Id, [operatorAddress]); + + // Operator signs the bad proof + const proof = await signAndEncodeProof( + operator, + e3Id, + REASON_MISBEHAVIOR, + verifierAddress, + ); + // Anyone can submit the signed evidence (permissionless for Lane A) await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), - ) - .to.emit(slashingManager, "SlashProposed") - .withArgs( - 0, - operatorAddress, - REASON_MISBEHAVIOR, - ethers.parseUnits("50", 6), - ethers.parseEther("100"), - currentTime + 1, - await slasher.getAddress(), - ); + .connect(proposer) + .proposeSlash(e3Id, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.emit(slashingManager, "SlashProposed"); + // Proof-based slashes auto-execute const proposal = await slashingManager.getSlashProposal(0); expect(proposal.operator).to.equal(operatorAddress); expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); expect(proposal.proofVerified).to.be.true; - expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.executed).to.be.true; + expect(proposal.proposer).to.equal(await proposer.getAddress()); }); - it("should propose slash without proof (evidence-based)", async function () { - const { slashingManager, slasher, operatorAddress } = - await loadFixture(setup); + it("should revert if circuit verifier says proof is valid (no fault)", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); - const evidencePolicy = { - ticketPenalty: ethers.parseUnits("20", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: false, - proofVerifier: ethers.ZeroAddress, + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, banNode: false, - appealWindow: APPEAL_WINDOW, + appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; - await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + // Set mock verifier to return true → proof is valid → NOT a fault + await mockVerifier.setReturnValue(true); - const proof = ethers.toUtf8Bytes(""); - const currentTime = await time.latest(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_INACTIVITY, proof), - ) - .to.emit(slashingManager, "SlashProposed") - .withArgs( - 0, - operatorAddress, - REASON_INACTIVITY, - ethers.parseUnits("20", 6), - ethers.parseEther("50"), - currentTime + APPEAL_WINDOW + 1, - await slasher.getAddress(), - ); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "ProofIsValid"); + }); - const proposal = await slashingManager.getSlashProposal(0); - expect(proposal.proofVerified).to.be.false; - expect(proposal.executableAt).to.be.greaterThan( - currentTime + APPEAL_WINDOW, + it("should revert if signer is not the operator (V-001 fix)", async function () { + const { + slashingManager, + proposer, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 0, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + // Proposer signs the proof (NOT the operator) — should be rejected + const proof = await signAndEncodeProof( + proposer, + 0, + REASON_MISBEHAVIOR, + verifierAddress, ); + await expect( + slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "SignerIsNotOperator"); }); - it("should revert if caller is not slasher", async function () { - const { slashingManager, notTheOwner, operatorAddress } = - await loadFixture(setup); + it("should revert if operator is not in committee (V-001 fix)", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 0, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const proof = ethers.toUtf8Bytes("Some proof"); + // Do NOT add operator to committee — empty committee for this E3 + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await expect( slashingManager - .connect(notTheOwner) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), - ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError( + slashingManager, + "OperatorNotInCommittee", + ); }); it("should revert if operator is zero address", async function () { - const { slashingManager, slasher } = await loadFixture(setup); + const { slashingManager, proposer, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Some proof"); + // Any non-empty proof triggers ZeroAddress check before decode + const proof = encodeDummyProof(); await expect( slashingManager - .connect(slasher) - .proposeSlash(ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), + .connect(proposer) + .proposeSlash(0, ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); }); it("should revert if slash reason is disabled", async function () { - const { slashingManager, slasher, operatorAddress } = + const { slashingManager, proposer, operatorAddress } = await loadFixture(setup); - const proof = ethers.toUtf8Bytes("Some proof"); + const proof = encodeDummyProof(); await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof), + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_DOUBLE_SIGN, proof), ).to.be.revertedWithCustomError(slashingManager, "SlashReasonDisabled"); }); - it("should revert if proof required but not provided", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = + it("should revert if proof is empty", async function () { + const { slashingManager, proposer, operatorAddress, mockVerifier } = await loadFixture(setup); const proofPolicy = { @@ -619,101 +793,210 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const emptyProof = ethers.toUtf8Bytes(""); - await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, emptyProof), + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, "0x"), ).to.be.revertedWithCustomError(slashingManager, "ProofRequired"); }); - it("should increment totalProposals", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + it("should reject duplicate evidence", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + const verifierAddress = await mockVerifier.getAddress(); const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: verifierAddress, banNode: false, appealWindow: 0, enabled: true, - }; - const evidencePolicy = { - ticketPenalty: ethers.parseUnits("20", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: false, - proofVerifier: ethers.ZeroAddress, - banNode: false, - appealWindow: APPEAL_WINDOW, - enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); + + // Same proof for same e3Id/operator/reason should be rejected + await expect( + slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "DuplicateEvidence"); + }); + + it("should increment totalProposals", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + await mockCiphernodeRegistry.setCommitteeNodes(1, [operatorAddress]); expect(await slashingManager.totalProposals()).to.equal(0); - const proof = ethers.toUtf8Bytes("Valid proof"); + const proof1 = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + "0x1111", + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof1); expect(await slashingManager.totalProposals()).to.equal(1); + const proof2 = await signAndEncodeProof( + operator, + 1, + REASON_MISBEHAVIOR, + verifierAddress, + "0x2222", + ); await slashingManager - .connect(slasher) - .proposeSlash( - operatorAddress, - REASON_INACTIVITY, - ethers.toUtf8Bytes(""), - ); + .connect(proposer) + .proposeSlash(1, operatorAddress, REASON_MISBEHAVIOR, proof2); expect(await slashingManager.totalProposals()).to.equal(2); }); + + it("should ban node when policy requires it", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + + const proof = await signAndEncodeProof( + operator, + 0, + REASON_DOUBLE_SIGN, + verifierAddress, + "0x3333", + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_DOUBLE_SIGN, proof); + + // banNode=true → auto-executed → node is now banned + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); }); - describe("executeSlash()", function () { - it("should execute slash with proof immediately", async function () { + describe("proposeSlashEvidence() — Lane B (evidence-based, SLASHER_ROLE)", function () { + it("should propose evidence-based slash with appeal window", async function () { const { slashingManager, slasher, operatorAddress, mockVerifier } = await loadFixture(setup); - const proofPolicy = { - ticketPenalty: ethers.parseUnits("50", 6), - licensePenalty: ethers.parseEther("100"), - requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), - banNode: false, - appealWindow: 0, - enabled: true, - }; - await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Valid proof"); - await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + const evidence = ethers.toUtf8Bytes("operator was inactive during E3"); + const e3Id = 0; - await expect(slashingManager.connect(slasher).executeSlash(0)) - .to.emit(slashingManager, "SlashExecuted") - .withArgs( - 0, - operatorAddress, - REASON_MISBEHAVIOR, - ethers.parseUnits("50", 6), - ethers.parseEther("100"), - true, - ); + await expect( + slashingManager + .connect(slasher) + .proposeSlashEvidence( + e3Id, + operatorAddress, + REASON_INACTIVITY, + evidence, + ), + ).to.emit(slashingManager, "SlashProposed"); const proposal = await slashingManager.getSlashProposal(0); - expect(proposal.executed).to.be.true; + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_INACTIVITY); + expect(proposal.proofVerified).to.be.false; + expect(proposal.executed).to.be.false; + expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.executableAt).to.be.gt(proposal.proposedAt); + }); + + it("should revert if caller is not slasher", async function () { + const { slashingManager, notTheOwner, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const evidence = ethers.toUtf8Bytes("evidence"); + + await expect( + slashingManager + .connect(notTheOwner) + .proposeSlashEvidence( + 0, + operatorAddress, + REASON_INACTIVITY, + evidence, + ), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); }); - it("should execute slash after appeal window expires", async function () { + it("should revert if operator is zero address", async function () { + const { slashingManager, slasher, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await expect( + slashingManager + .connect(slasher) + .proposeSlashEvidence( + 0, + ethers.ZeroAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + }); + + describe("executeSlash() — Lane B execution", function () { + it("should execute evidence-based slash after appeal window", async function () { const { slashingManager, slasher, operatorAddress, mockVerifier } = await loadFixture(setup); @@ -721,49 +1004,66 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); + // Should revert before appeal window expires await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealWindowActive"); + // Fast forward past appeal window await time.increase(APPEAL_WINDOW + 1); - await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + await expect(slashingManager.executeSlash(0)).to.emit( slashingManager, "SlashExecuted", ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.executed).to.be.true; }); - it("should ban node when policy requires it", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + it("should revert if proof-based slash tries to executeSlash separately", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); - const proof = ethers.toUtf8Bytes("Serious violation proof"); + // Proof-based slash auto-executes in proposeSlash + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof); - - expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); - await expect(slashingManager.connect(slasher).executeSlash(0)) - .to.emit(slashingManager, "NodeBanUpdated") - .withArgs(operatorAddress, true, REASON_DOUBLE_SIGN, slasher); - - expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + // Should revert because already executed + await expect( + slashingManager.executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); }); it("should revert if proposal doesn't exist", async function () { - const { slashingManager, slasher } = await loadFixture(setup); + const { slashingManager } = await loadFixture(setup); await expect( - slashingManager.connect(slasher).executeSlash(999), + slashingManager.executeSlash(999), ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); }); @@ -773,20 +1073,26 @@ describe("SlashingManager", function () { await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Valid proof"); await slashingManager .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); - await slashingManager.connect(slasher).executeSlash(0); + .proposeSlashEvidence( + 0, + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes("evidence"), + ); + + await time.increase(APPEAL_WINDOW + 1); + await slashingManager.executeSlash(0); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); }); }); describe("appeal system", function () { - it("should allow operator to file appeal", async function () { + it("should allow operator to file appeal on evidence-based slash", async function () { const { slashingManager, slasher, @@ -799,10 +1105,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); const evidence = "I was not inactive, here's the proof..."; @@ -828,10 +1135,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await expect( @@ -852,10 +1160,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await time.increase(APPEAL_WINDOW + 1); @@ -878,10 +1187,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "First appeal"); @@ -891,6 +1201,37 @@ describe("SlashingManager", function () { ).to.be.revertedWithCustomError(slashingManager, "AlreadyAppealed"); }); + it("should revert if appealing proof-verified slash", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + // Proof-based slash auto-executes with proofVerified=true + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); + + // Cannot appeal proof-verified slashes — appeal window is 0 so it's already expired + await expect( + slashingManager.connect(operator).fileAppeal(0, "Cannot appeal proof"), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowExpired"); + }); + it("should allow governance to resolve appeal (approve)", async function () { const { slashingManager, @@ -905,10 +1246,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); @@ -945,10 +1287,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); @@ -974,17 +1317,18 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await time.increase(APPEAL_WINDOW + 1); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealPending"); }); @@ -1002,10 +1346,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, true, "Approved"); @@ -1013,7 +1358,7 @@ describe("SlashingManager", function () { await time.increase(APPEAL_WINDOW + 1); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealUpheld"); }); @@ -1031,17 +1376,18 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, false, "Denied"); await time.increase(APPEAL_WINDOW + 1); - await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + await expect(slashingManager.executeSlash(0)).to.emit( slashingManager, "SlashExecuted", ); @@ -1127,6 +1473,8 @@ describe("SlashingManager", function () { banNode: true, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy); @@ -1140,27 +1488,44 @@ describe("SlashingManager", function () { expect(retrieved.banNode).to.equal(policy.banNode); expect(retrieved.appealWindow).to.equal(policy.appealWindow); expect(retrieved.enabled).to.equal(policy.enabled); + expect(retrieved.affectsCommittee).to.equal(policy.affectsCommittee); + expect(retrieved.failureReason).to.equal(policy.failureReason); }); it("should return correct slash proposal", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); - const proof = ethers.toUtf8Bytes("test proof"); + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + "0x4444", + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); const proposal = await slashingManager.getSlashProposal(0); expect(proposal.operator).to.equal(operatorAddress); expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); expect(proposal.ticketAmount).to.equal(ethers.parseUnits("50", 6)); expect(proposal.licenseAmount).to.equal(ethers.parseEther("100")); - expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.proposer).to.equal(await proposer.getAddress()); expect(proposal.proofHash).to.equal(ethers.keccak256(proof)); expect(proposal.proofVerified).to.be.true; + expect(proposal.executed).to.be.true; }); it("should revert for invalid proposal ID", async function () { diff --git a/templates/default/client/src/context/WizardContext.tsx b/templates/default/client/src/context/WizardContext.tsx index 2a239998ec..32c6fbbd21 100644 --- a/templates/default/client/src/context/WizardContext.tsx +++ b/templates/default/client/src/context/WizardContext.tsx @@ -13,6 +13,7 @@ import { getEnclaveSDKConfig } from '@/utils/sdk-config' // TYPES & ENUMS // ============================================================================ +// eslint-disable-next-line react-refresh/only-export-components export enum WizardStep { CONNECT_WALLET = 1, REQUEST_COMPUTATION = 2, @@ -61,6 +62,7 @@ interface WizardContextType { const WizardContext = createContext(undefined) +// eslint-disable-next-line react-refresh/only-export-components export const useWizard = () => { const context = useContext(WizardContext) if (!context) { @@ -104,6 +106,7 @@ export const WizardProvider: React.FC = ({ children }) => { }) // Auto-advance steps based on state. + /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!isConnected) { setCurrentStep(WizardStep.CONNECT_WALLET) @@ -111,6 +114,7 @@ export const WizardProvider: React.FC = ({ children }) => { setCurrentStep(WizardStep.REQUEST_COMPUTATION) } }, [isConnected, sdk.isInitialized, currentStep]) + /* eslint-enable react-hooks/set-state-in-effect */ const handleReset = useCallback(() => { setCurrentStep(WizardStep.CONNECT_WALLET) diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index 2b06cbc524..efbb0015a2 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -75,7 +75,7 @@ const RequestComputation: React.FC = () => { off(EnclaveEventType.E3_REQUESTED, handleE3Requested) off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) } - }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType]) + }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType, setE3State]) // Auto-advance to next step when committee publishes useEffect(() => { diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index aaa84e54db..16d03bf0f6 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690" + address: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9" deploy_block: 1 # Set to actual deploy block enclave: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" @@ -14,6 +14,9 @@ chains: bonding_registry: address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block + slashing_manager: + address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' + deploy_block: 8 fee_token: address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" deploy_block: 1 # Set to actual deploy block From 3a49d3ccd5574b0386d5a1e5ac67402f964a9499 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 14:53:13 +0500 Subject: [PATCH 02/21] chore: trigger CI From f5062b33429127c5d369818d863c808a9dd85d22 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 15:17:35 +0500 Subject: [PATCH 03/21] fix: review comments --- .../contracts/interfaces/ICircuitVerifier.sol | 2 +- .../contracts/registry/BondingRegistry.sol | 23 +++---- .../scripts/deployEnclave.ts | 3 + .../test/Slashing/CommitteeExpulsion.spec.ts | 1 + .../client/src/context/WizardContext.tsx | 60 +++++++++---------- 5 files changed, 42 insertions(+), 47 deletions(-) diff --git a/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol index ffca72f19b..0ff4008516 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol @@ -19,5 +19,5 @@ interface ICircuitVerifier { function verify( bytes calldata _proof, bytes32[] calldata _publicInputs - ) external returns (bool); + ) external view returns (bool); } diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 53eff07658..fc27c00bf6 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -9,9 +9,6 @@ pragma solidity >=0.8.27; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import { - ReentrancyGuardUpgradeable -} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 @@ -33,7 +30,6 @@ import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; contract BondingRegistry is IBondingRegistry, OwnableUpgradeable, - ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; using ExitQueueLib for ExitQueueLib.ExitQueueState; @@ -184,7 +180,6 @@ contract BondingRegistry is uint64 _exitDelay ) public initializer { __Ownable_init(msg.sender); - __ReentrancyGuard_init(); setTicketToken(_ticketToken); setLicenseToken(_licenseToken); setRegistry(_registry); @@ -316,7 +311,7 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function deregisterOperator( uint256[] calldata siblingNodes - ) external noExitInProgress(msg.sender) nonReentrant { + ) external noExitInProgress(msg.sender) { Operator storage op = operators[msg.sender]; require(op.registered, NotRegistered()); @@ -364,7 +359,7 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function addTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) nonReentrant { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); @@ -383,7 +378,7 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function removeTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) nonReentrant { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -405,9 +400,7 @@ contract BondingRegistry is } /// @inheritdoc IBondingRegistry - function bondLicense( - uint256 amount - ) external noExitInProgress(msg.sender) nonReentrant { + function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); uint256 balanceBefore = licenseToken.balanceOf(address(this)); @@ -430,7 +423,7 @@ contract BondingRegistry is /// @inheritdoc IBondingRegistry function unbondLicense( uint256 amount - ) external noExitInProgress(msg.sender) nonReentrant { + ) external noExitInProgress(msg.sender) { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -458,7 +451,7 @@ contract BondingRegistry is function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount - ) external nonReentrant { + ) external { (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, maxTicketAmount, @@ -587,7 +580,7 @@ contract BondingRegistry is IERC20 rewardToken, address[] calldata recipients, uint256[] calldata amounts - ) external nonReentrant { + ) external { require(authorizedDistributors[msg.sender], OnlyRewardDistributor()); require(recipients.length == amounts.length, ArrayLengthMismatch()); @@ -713,7 +706,7 @@ contract BondingRegistry is function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount - ) public onlyOwner nonReentrant { + ) public onlyOwner { require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 8da5a2668d..10409bc55e 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -196,6 +196,9 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting Enclave address in SlashingManager..."); await slashingManager.setEnclave(enclaveAddress); + console.log("Setting SlashingManager address in Enclave..."); + await enclave.setSlashingManager(slashingManagerAddress); + console.log("Setting SlashingManager address in BondingRegistry..."); await bondingRegistry.setSlashingManager(slashingManagerAddress); diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts index ba1918e6fd..7bcea8071d 100644 --- a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -73,6 +73,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; const defaultTimeoutConfig = { + committeeFormationWindow: ONE_DAY, dkgWindow: ONE_DAY, computeWindow: THREE_DAYS, decryptionWindow: ONE_DAY, diff --git a/templates/default/client/src/context/WizardContext.tsx b/templates/default/client/src/context/WizardContext.tsx index 32c6fbbd21..86361284bb 100644 --- a/templates/default/client/src/context/WizardContext.tsx +++ b/templates/default/client/src/context/WizardContext.tsx @@ -34,6 +34,17 @@ export interface E3State { hasPlaintextOutput: boolean } +const INITIAL_E3_STATE: E3State = { + id: null, + isRequested: false, + isCommitteePublished: false, + isActivated: false, + publicKey: null, + expiresAt: null, + plaintextOutput: null, + hasPlaintextOutput: false, +} + interface WizardContextType { currentStep: WizardStep submittedInputs: { input1: string; input2: string } | null @@ -94,46 +105,33 @@ export const WizardProvider: React.FC = ({ children }) => { const [inputPublishError, setInputPublishError] = useState(null) const [inputPublishSuccess, setInputPublishSuccess] = useState(false) const [result, setResult] = useState(null) - const [e3State, setE3State] = useState({ - id: null, - isRequested: false, - isCommitteePublished: false, - isActivated: false, - publicKey: null, - expiresAt: null, - plaintextOutput: null, - hasPlaintextOutput: false, - }) - - // Auto-advance steps based on state. + const [e3State, setE3State] = useState(INITIAL_E3_STATE) + + const resetWizardState = useCallback((step: WizardStep) => { + setCurrentStep(step) + setSubmittedInputs(null) + setLastTransactionHash(undefined) + setInputPublishError(null) + setInputPublishSuccess(false) + setResult(null) + setE3State(INITIAL_E3_STATE) + }, []) + + // Auto-advance steps based on connection & SDK state. /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!isConnected) { - setCurrentStep(WizardStep.CONNECT_WALLET) + resetWizardState(WizardStep.CONNECT_WALLET) } else if (sdk.isInitialized && currentStep === WizardStep.CONNECT_WALLET) { setCurrentStep(WizardStep.REQUEST_COMPUTATION) } - }, [isConnected, sdk.isInitialized, currentStep]) + }, [isConnected, sdk.isInitialized, currentStep, resetWizardState]) /* eslint-enable react-hooks/set-state-in-effect */ const handleReset = useCallback(() => { - setCurrentStep(WizardStep.CONNECT_WALLET) - setSubmittedInputs(null) - setLastTransactionHash(undefined) - setInputPublishError(null) - setInputPublishSuccess(false) - setResult(null) - setE3State({ - id: null, - isRequested: false, - isCommitteePublished: false, - isActivated: false, - publicKey: null, - expiresAt: null, - plaintextOutput: null, - hasPlaintextOutput: false, - }) - }, []) + const step = isConnected && sdk.isInitialized ? WizardStep.REQUEST_COMPUTATION : WizardStep.CONNECT_WALLET + resetWizardState(step) + }, [isConnected, sdk.isInitialized, resetWizardState]) const handleTryAgain = useCallback(() => { setCurrentStep(WizardStep.ENTER_INPUTS) From 183963281cfd85f1ca4ed71428322d21e03d514a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 15:30:57 +0500 Subject: [PATCH 04/21] fix: remove comments --- packages/enclave-contracts/contracts/E3RefundManager.sol | 6 +++--- .../contracts/slashing/SlashingManager.sol | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index e3ed6a7e1a..d7e03481a9 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -44,11 +44,11 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { /// @notice Tracks claims per E3 per address mapping(uint256 e3Id => mapping(address claimer => bool hasClaimed)) internal _claimed; - /// @notice Tracks number of claims made per E3 (for routeSlashedFunds guard) + /// @notice Tracks number of claims made per E3 mapping(uint256 e3Id => uint256 count) internal _claimCount; - /// @notice Tracks number of honest node claims made per E3 (for dust fix) + /// @notice Tracks number of honest node claims made per E3 mapping(uint256 e3Id => uint256 count) internal _honestNodeClaimCount; - /// @notice Tracks total amount paid to honest nodes per E3 (for dust fix) + /// @notice Tracks total amount paid to honest nodes per E3 mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses mapping(uint256 e3Id => address[] nodes) internal _honestNodes; diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 7cbfd2c680..3e8e148aa3 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -254,7 +254,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(policy.requiresProof, InvalidPolicy()); require(proof.length != 0, ProofRequired()); - // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + // Evidence replay protection — reason-independent to prevent cross-reason replay bytes32 evidenceKey = keccak256( abi.encode(e3Id, operator, keccak256(proof)) ); @@ -279,7 +279,6 @@ contract SlashingManager is ISlashingManager, AccessControl { p.proposer = msg.sender; p.proofHash = keccak256(proof); p.proofVerified = true; - // Snapshot behavioral flags from policy at proposal time p.banNode = policy.banNode; p.affectsCommittee = policy.affectsCommittee; p.failureReason = policy.failureReason; @@ -312,7 +311,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(policy.enabled, SlashReasonDisabled()); require(!policy.requiresProof, InvalidPolicy()); - // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + // Evidence replay protection — reason-independent to prevent cross-reason replay bytes32 evidenceKey = keccak256( abi.encode(e3Id, operator, keccak256(evidence)) ); @@ -447,7 +446,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // 4. Re-verify the ZK proof on-chain (INVERTED: must FAIL to confirm fault). // The staticcall MUST succeed — if the verifier reverts or doesn't exist, - // we cannot determine fault and must not slash (M-04 fix). + // we cannot determine fault and must not slash. (bool callSuccess, bytes memory returnData) = policyVerifier.staticcall( abi.encodeCall(ICircuitVerifier.verify, (zkProof, publicInputs)) ); From 150a1679919ff41715782c94e1cecbb2c393d9b8 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 15:38:11 +0500 Subject: [PATCH 05/21] fix: remove comments --- .../enclave-contracts/contracts/registry/BondingRegistry.sol | 5 +---- .../enclave-contracts/contracts/slashing/SlashingManager.sol | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index fc27c00bf6..27a865a97f 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -27,10 +27,7 @@ import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; * @dev Handles deposits, withdrawals, slashing, exits, and integrates with registry and slashing manager */ // solhint-disable-next-line max-states-count -contract BondingRegistry is - IBondingRegistry, - OwnableUpgradeable, -{ +contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { using SafeERC20 for IERC20; using ExitQueueLib for ExitQueueLib.ExitQueueState; diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 3e8e148aa3..3fd872c9bf 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -501,6 +501,7 @@ contract SlashingManager is ISlashingManager, AccessControl { // If active count drops below M, fail the E3 if (activeCount < thresholdM && p.failureReason > 0) { + // solhint-disable-next-line no-empty-blocks try enclave.onE3Failed(p.e3Id, p.failureReason) {} catch {} } } From 60143559eb3f23b15bfd939c9956c08735fb27a4 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 15:58:02 +0500 Subject: [PATCH 06/21] fix: contract addresses --- examples/CRISP/enclave.config.yaml | 2 +- examples/CRISP/server/.env.example | 2 +- templates/default/enclave.config.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 71e18d12b6..be50309cb8 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: ws://localhost:8545 contracts: e3_program: - address: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8' + address: '0x851356ae760d987E095750cCeb3bC6014560891C' deploy_block: 37 enclave: address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 58d0f1c6e4..6eff98ebc8 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,7 +15,7 @@ CRON_API_KEY=1234567890 # Based on Default Hardhat Deployments (Only for testing) ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" CIPHERNODE_REGISTRY_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x851356ae760d987E095750cCeb3bC6014560891C" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 16d03bf0f6..dd0161545a 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9" + address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" deploy_block: 1 # Set to actual deploy block enclave: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" From a4637d0a692eefbef3efceee1088ae0eeb4c7805 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 19:03:00 +0500 Subject: [PATCH 07/21] fix: add retry to metamask --- examples/CRISP/test/crisp.spec.ts | 36 +++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 0f26a010dc..3de2bdf12b 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -68,6 +68,36 @@ function log(msg: string) { console.log(`[playwright] ${msg}`) } +// ConnectKit modal animations + app initialization (initialLoad/switchChain) +// can cause the MetaMask button to be detached from the DOM or the page to +// navigate while the modal is opening. Retry the whole flow up to 3 times. +async function connectWalletWithRetry(page: Page, maxAttempts = 3) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await page.waitForLoadState('load') + + const connectWalletBtn = page.locator('button:has-text("Connect Wallet")') + const metamaskBtn = page.locator('button:has-text("MetaMask")') + + // Only open the modal if MetaMask option isn't already visible + if (!(await metamaskBtn.isVisible().catch(() => false))) { + log(`clicking Connect Wallet (attempt ${attempt})...`) + await connectWalletBtn.click({ timeout: 10_000 }) + } + + log(`clicking MetaMask (attempt ${attempt})...`) + await metamaskBtn.click({ timeout: 15_000 }) + return + } catch (error) { + if (attempt === maxAttempts) throw error + log(`wallet connect attempt ${attempt} failed, retrying...`) + // Dismiss any open modal before retrying + await page.keyboard.press('Escape').catch(() => {}) + await page.waitForTimeout(2_000) + } + } +} + test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => { page.on('console', (msg: ConsoleMessage) => { console.log(msg.text()) @@ -90,10 +120,8 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => log(`ensureHomePageLoaded...`) await ensureHomePageLoaded(page) - log(`searching for connect button...`) - await page.locator('button:has-text("Connect Wallet")').click() - log(`searching for MetaMask button...`) - await page.locator('button:has-text("MetaMask")').click() + log(`connecting wallet via ConnectKit...`) + await connectWalletWithRetry(page) log(`connecting to dapp...`) await metamask.connectToDapp() log(`clicking try demo...`) From 7c241a0cada3fb03e10a7386de8fa0b6a274b134 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 20:22:00 +0500 Subject: [PATCH 08/21] fix: review comments --- .../contracts/E3RefundManager.sol | 13 ++++++---- .../enclave-contracts/contracts/Enclave.sol | 10 ++------ .../registry/CiphernodeRegistryOwnable.sol | 22 ++++++---------- .../contracts/slashing/SlashingManager.sol | 25 ++++--------------- 4 files changed, 22 insertions(+), 48 deletions(-) diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index d7e03481a9..aa4d8def71 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -288,16 +288,19 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { require(dist.honestNodeCount > 0, NoRefundAvailable(e3Id)); uint256 perNodeAmount = dist.honestNodeAmount / dist.honestNodeCount; + require(perNodeAmount > 0, NoRefundAvailable(e3Id)); + amount = perNodeAmount; _honestNodeClaimCount[e3Id]++; if (_honestNodeClaimCount[e3Id] == dist.honestNodeCount) { - // Last claimer gets whatever remains (includes dust) - amount = dist.honestNodeAmount - _totalHonestNodePaid[e3Id]; - } else { - amount = perNodeAmount; + // Route rounding remainder (dust) to protocol treasury. + uint256 dust = dist.honestNodeAmount - + (_totalHonestNodePaid[e3Id] + perNodeAmount); + if (dust > 0) { + dist.feeToken.safeTransfer(treasury, dust); + } } _totalHonestNodePaid[e3Id] += amount; - require(amount > 0, NoRefundAvailable(e3Id)); _claimed[e3Id][msg.sender] = true; _claimCount[e3Id]++; diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index d5e8abbbd6..288045cde2 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -657,11 +657,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { bytes[] memory _e3ProgramsParams ) public onlyOwner { uint256 length = _e3ProgramsParams.length; - for (uint256 i; i < length; ) { + for (uint256 i; i < length; ++i) { e3ProgramsParams[_e3ProgramsParams[i]] = true; - unchecked { - ++i; - } } emit AllowedE3ProgramsParamsSet(_e3ProgramsParams); } @@ -672,11 +669,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { bytes[] memory _e3ProgramsParams ) public onlyOwner { uint256 length = _e3ProgramsParams.length; - for (uint256 i; i < length; ) { + for (uint256 i; i < length; ++i) { delete e3ProgramsParams[_e3ProgramsParams[i]]; - unchecked { - ++i; - } } emit E3ProgramsParamsRemoved(_e3ProgramsParams); } diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index fc20835afd..de58b48fbc 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -399,11 +399,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { c.committee = c.topNodes; // Initialize active committee tracking in Committee struct uint256 committeeLen = c.committee.length; - for (uint256 i = 0; i < committeeLen; ) { + for (uint256 i = 0; i < committeeLen; ++i) { c.active[c.committee[i]] = true; - unchecked { - ++i; - } } c.activeCount = committeeLen; @@ -593,14 +590,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { address[] memory activeNodes = new address[](actCount); uint256 idx = 0; - for (uint256 i = 0; i < total; ) { + for (uint256 i = 0; i < total; ++i) { if (c.active[c.committee[i]]) { activeNodes[idx] = c.committee[i]; idx++; } - unchecked { - ++i; - } } return activeNodes; } @@ -705,13 +699,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint256 worstIdx = 0; uint256 worstScore = c.scoreOf[top[0]]; - unchecked { - for (uint256 i = 1; i < top.length; ++i) { - uint256 s = c.scoreOf[top[i]]; - if (s > worstScore) { - worstScore = s; - worstIdx = i; - } + for (uint256 i = 1; i < top.length; ++i) { + uint256 s = c.scoreOf[top[i]]; + if (s > worstScore) { + worstScore = s; + worstIdx = i; } } diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 3fd872c9bf..0f26369e72 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -374,24 +374,6 @@ contract SlashingManager is ISlashingManager, AccessControl { // Internal Execution // ====================== - /// @dev Verifies the operator is/was a committee member for the given E3. - function _verifyCommitteeMembership( - uint256 e3Id, - address operator - ) internal view { - address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( - e3Id - ); - bool isMember = false; - for (uint256 i = 0; i < committeeNodes.length; i++) { - if (committeeNodes[i] == operator) { - isMember = true; - break; - } - } - require(isMember, OperatorNotInCommittee()); - } - /// @dev Decodes evidence, verifies operator signature, committee membership, /// and that the ZK proof is invalid (fault confirmed). /// Evidence format: @@ -442,7 +424,10 @@ contract SlashingManager is ISlashingManager, AccessControl { require(recoveredSigner == operator, SignerIsNotOperator()); // 3. Verify committee membership. - _verifyCommitteeMembership(e3Id, operator); + require( + ciphernodeRegistry.isCommitteeMemberActive(e3Id, operator), + OperatorNotInCommittee() + ); // 4. Re-verify the ZK proof on-chain (INVERTED: must FAIL to confirm fault). // The staticcall MUST succeed — if the verifier reverts or doesn't exist, @@ -453,7 +438,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(callSuccess, VerifierCallFailed()); require(returnData.length >= 32, VerifierCallFailed()); bool proofValid = abi.decode(returnData, (bool)); - if (proofValid) revert ProofIsValid(); + require(!proofValid, ProofIsValid()); } /** From 23e8225c1875a1a7976d6716af53390ddec49f8e Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Mon, 23 Feb 2026 23:12:40 +0500 Subject: [PATCH 09/21] fix: review comments --- .../IBondingRegistry.json | 21 ++- .../ICiphernodeRegistry.json | 70 ++++++---- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../ISlashingManager.json | 22 ++- .../EnclaveTicketToken.json | 2 +- .../DkgPkVerifier.sol/DkgPkVerifier.json | 8 +- .../DkgPkVerifier.sol/ZKTranscriptLib.json | 2 +- .../enclave-contracts/contracts/Enclave.sol | 2 +- .../contracts/interfaces/IBondingRegistry.sol | 10 ++ .../interfaces/ICiphernodeRegistry.sol | 71 ++++++---- .../contracts/interfaces/IEnclave.sol | 4 +- .../contracts/registry/BondingRegistry.sol | 3 + .../registry/CiphernodeRegistryOwnable.sol | 125 ++++++++++++------ .../contracts/slashing/SlashingManager.sol | 10 +- .../contracts/test/MockCiphernodeRegistry.sol | 49 ++++--- .../test/Slashing/CommitteeExpulsion.spec.ts | 21 +-- 16 files changed, 294 insertions(+), 128 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index b87aa0ec93..dfdc8aa85b 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -172,6 +172,25 @@ "name": "OperatorActivationChanged", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "distributor", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "authorized", + "type": "bool" + } + ], + "name": "RewardDistributorUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -903,5 +922,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" + "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 28e91369d8..8e63f27784 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -394,25 +394,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getActiveCommitteeCount", - "outputs": [ - { - "internalType": "uint256", - "name": "count", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -491,12 +472,27 @@ "type": "uint256" } ], - "name": "getCommitteeThreshold", + "name": "getCommitteeViability", "outputs": [ { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" + "internalType": "uint256", + "name": "activeCount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "thresholdM", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "thresholdN", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "viable", + "type": "bool" } ], "stateMutability": "view", @@ -521,6 +517,30 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isCommitteeMember", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -538,7 +558,7 @@ "outputs": [ { "internalType": "bool", - "name": "active", + "name": "", "type": "bool" } ], @@ -767,5 +787,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" + "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 2bfe082fbd..d500ba38ba 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -1226,5 +1226,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" + "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json index 6ba870c913..1744600bb4 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -38,6 +38,11 @@ "name": "AppealWindowExpired", "type": "error" }, + { + "inputs": [], + "name": "ChainIdMismatch", + "type": "error" + }, { "inputs": [], "name": "CiphernodeBanned", @@ -591,6 +596,21 @@ "internalType": "bool", "name": "proofVerified", "type": "bool" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" } ], "internalType": "struct ISlashingManager.SlashProposal", @@ -845,5 +865,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", - "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" + "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index 17d318e4f7..742068c990 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -1223,5 +1223,5 @@ ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" + "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json index 49bba848aa..7b356ea230 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "33764": [ + "33966": [ { "length": 32, "start": 91 @@ -164,13 +164,13 @@ "start": 11964 } ], - "33766": [ + "33968": [ { "length": 32, "start": 398 } ], - "33768": [ + "33970": [ { "length": 32, "start": 432 @@ -182,5 +182,5 @@ ] }, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" + "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json index 77f4e0d705..d13bc6ac01 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json @@ -396,5 +396,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" + "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 288045cde2..75d7af278a 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -778,7 +778,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { uint8 reason ) external onlyCiphernodeRegistryOrSlashingManager { require( - reason > 0 && reason <= uint8(FailureReason.VerificationFailed), + reason > 0 && reason <= uint8(FailureReason._MAX_FAILURE_REASON), "Invalid failure reason" ); // Mark E3 as failed with the given reason diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index d910cdb5ff..61b6fa0fb8 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -97,6 +97,16 @@ interface IBondingRegistry { uint256 newValue ); + /** + * @notice Emitted when a reward distributor is authorized or revoked + * @param distributor Address of the distributor + * @param authorized True if authorized, false if revoked + */ + event RewardDistributorUpdated( + address indexed distributor, + bool authorized + ); + /** * @notice Emitted when treasury withdraws slashed funds * @param to Treasury address diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 69b12a8752..f6f002d828 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -15,33 +15,43 @@ import { IBondingRegistry } from "./IBondingRegistry.sol"; * and coordinates committee selection for E3 computations */ interface ICiphernodeRegistry { + /// @notice Tracks a committee member's lifecycle state for a given E3. + enum MemberStatus { + None, + Active, + Expelled + } + + /// @notice Lifecycle stage of a committee for a given E3. + enum CommitteeStage { + None, + Requested, + Finalized, + Failed + } + /// @notice Struct representing the sortition state for an E3 round. - /// @param initialized Whether the round has been initialized. - /// @param finalized Whether the round has been finalized. + /// @param stage Current lifecycle stage of the committee (replaces former initialized/finalized/failed bools). /// @param requestBlock The block number when the committee was requested. /// @param committeeDeadline The deadline for committee formation (ticket submission). /// @param threshold The M/N threshold for the committee ([M, N]). /// @param publicKey Hash of the committee's public key. /// @param seed The seed for the round. - /// @param topNodes The top nodes in the round. - /// @param committee The committee for the round. + /// @param topNodes Sorted top-N nodes selected during sortition. /// @param submitted Mapping of nodes to their submission status. /// @param scoreOf Mapping of nodes to their scores. - /// @param failed True if committee formation failed (threshold not met). + /// @param memberStatus Tri-state membership tracking (None / Active / Expelled). struct Committee { - bool initialized; - bool finalized; - bool failed; + CommitteeStage stage; uint256 seed; uint256 requestBlock; uint256 committeeDeadline; bytes32 publicKey; uint32[2] threshold; address[] topNodes; - address[] committee; mapping(address node => bool submitted) submitted; mapping(address node => uint256 score) scoreOf; - mapping(address node => bool active) active; + mapping(address node => MemberStatus) memberStatus; uint256 activeCount; } @@ -279,7 +289,7 @@ interface ICiphernodeRegistry { /// @notice Expel a committee member from a specific E3 committee due to slashing /// @dev Only callable by SlashingManager. Idempotent (re-expelling same member is no-op). /// Returns viability data so the caller can decide whether to fail the E3 — - /// eliminating the need for separate getActiveCommitteeCount/getCommitteeThreshold calls. + /// eliminating the need for separate view calls to check count and threshold. /// @param e3Id ID of the E3 computation /// @param node Address of the committee member to expel /// @param reason Hash of the slash reason @@ -294,11 +304,20 @@ interface ICiphernodeRegistry { /// @notice Check if a committee member is still active for a specific E3 /// @param e3Id ID of the E3 computation /// @param node Address of the committee member to check - /// @return active Whether the member is still active in the committee + /// @return Whether the member is still active (not expelled) in the committee function isCommitteeMemberActive( uint256 e3Id, address node - ) external view returns (bool active); + ) external view returns (bool); + + /// @notice Check if an address was ever a committee member for a specific E3 + /// @param e3Id ID of the E3 computation + /// @param node Address to check + /// @return Whether the address was ever a member of the finalized committee + function isCommitteeMember( + uint256 e3Id, + address node + ) external view returns (bool); /// @notice Get active (non-expelled) committee nodes for an E3 /// @param e3Id ID of the E3 computation @@ -307,17 +326,21 @@ interface ICiphernodeRegistry { uint256 e3Id ) external view returns (address[] memory nodes); - /// @notice Get the count of active committee members for an E3 - /// @param e3Id ID of the E3 computation - /// @return count Number of active committee members - function getActiveCommitteeCount( - uint256 e3Id - ) external view returns (uint256 count); - - /// @notice Get the threshold configuration for an E3 committee + /// @notice Consolidated committee viability check — avoids two separate view calls. /// @param e3Id ID of the E3 computation - /// @return threshold The [M, N] threshold array - function getCommitteeThreshold( + /// @return activeCount Current number of active (non-expelled) committee members + /// @return thresholdM Minimum required members (M in M-of-N) + /// @return thresholdN Total desired committee size (N in M-of-N) + /// @return viable True when activeCount >= thresholdM + function getCommitteeViability( uint256 e3Id - ) external view returns (uint32[2] memory threshold); + ) + external + view + returns ( + uint256 activeCount, + uint32 thresholdM, + uint32 thresholdN, + bool viable + ); } diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index f96d76a2a0..758202a906 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -32,6 +32,7 @@ interface IEnclave { } /// @notice Reasons why an E3 failed + /// @dev Any new failure reason should be added before _MAX_FAILURE_REASON. enum FailureReason { None, CommitteeFormationTimeout, @@ -45,7 +46,8 @@ interface IEnclave { RequesterCancelled, DecryptionTimeout, DecryptionInvalidShares, - VerificationFailed + VerificationFailed, + _MAX_FAILURE_REASON } //////////////////////////////////////////////////////////// diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 27a865a97f..d9fcbb6ad7 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -689,7 +689,9 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { function setRewardDistributor( address newRewardDistributor ) public onlyOwner { + require(newRewardDistributor != address(0), ZeroAddress()); authorizedDistributors[newRewardDistributor] = true; + emit RewardDistributorUpdated(newRewardDistributor, true); } /// @notice Revokes reward distributor authorization @@ -697,6 +699,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @param distributor Address to revoke function revokeRewardDistributor(address distributor) public onlyOwner { authorizedDistributors[distributor] = false; + emit RewardDistributorUpdated(distributor, false); } /// @inheritdoc IBondingRegistry diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index de58b48fbc..bd2a5bbff6 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -237,7 +237,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint32[2] calldata threshold ) external onlyEnclave returns (bool success) { Committee storage c = committees[e3Id]; - require(!c.initialized, CommitteeAlreadyRequested()); + require( + c.stage == ICiphernodeRegistry.CommitteeStage.None, + CommitteeAlreadyRequested() + ); uint256 activeCount = bondingRegistry.numActiveOperators(); require( @@ -245,8 +248,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { InsufficientCiphernodes(threshold[1], activeCount) ); - c.initialized = true; - c.finalized = false; + c.stage = ICiphernodeRegistry.CommitteeStage.Requested; c.seed = seed; c.requestBlock = block.number; c.committeeDeadline = block.timestamp + sortitionSubmissionWindow; @@ -277,10 +279,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { ) external onlyOwner { Committee storage c = committees[e3Id]; - require(c.initialized, CommitteeNotRequested()); - require(c.finalized, CommitteeNotFinalized()); + require( + c.stage != ICiphernodeRegistry.CommitteeStage.None, + CommitteeNotRequested() + ); + require( + c.stage == ICiphernodeRegistry.CommitteeStage.Finalized, + CommitteeNotFinalized() + ); require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); - require(nodes.length == c.committee.length, "Node count mismatch"); + require(nodes.length == c.topNodes.length, "Node count mismatch"); // TODO: Currently we trust the owner to publish the correct committee. // TODO: Need a Proof that the public key is generated from the committee @@ -338,8 +346,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @param ticketNumber The ticket number to submit (1 to available tickets at snapshot) function submitTicket(uint256 e3Id, uint256 ticketNumber) external { Committee storage c = committees[e3Id]; - require(c.initialized, CommitteeNotRequested()); - require(!c.finalized, CommitteeAlreadyFinalized()); + require( + c.stage != ICiphernodeRegistry.CommitteeStage.None, + CommitteeNotRequested() + ); + require( + c.stage == ICiphernodeRegistry.CommitteeStage.Requested, + CommitteeAlreadyFinalized() + ); require( block.timestamp <= c.committeeDeadline, CommitteeDeadlineReached() @@ -373,17 +387,22 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @return success True if committee formed successfully, false if threshold not met function finalizeCommittee(uint256 e3Id) external returns (bool success) { Committee storage c = committees[e3Id]; - require(c.initialized, CommitteeNotRequested()); - require(!c.finalized, CommitteeAlreadyFinalized()); + require( + c.stage != ICiphernodeRegistry.CommitteeStage.None, + CommitteeNotRequested() + ); + require( + c.stage == ICiphernodeRegistry.CommitteeStage.Requested, + CommitteeAlreadyFinalized() + ); require( block.timestamp > c.committeeDeadline, SubmissionWindowNotClosed() ); - c.finalized = true; bool thresholdMet = c.topNodes.length >= c.threshold[1]; if (!thresholdMet) { - c.failed = true; + c.stage = ICiphernodeRegistry.CommitteeStage.Failed; emit CommitteeFormationFailed( e3Id, c.topNodes.length, @@ -396,11 +415,15 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return false; } - c.committee = c.topNodes; - // Initialize active committee tracking in Committee struct - uint256 committeeLen = c.committee.length; + c.stage = ICiphernodeRegistry.CommitteeStage.Finalized; + // Mark every finalized member as Active. + // Active → counts toward viability, earns rewards. + // Expelled → was a member (enables re-slash via Lane A) but no longer counts. + uint256 committeeLen = c.topNodes.length; for (uint256 i = 0; i < committeeLen; ++i) { - c.active[c.committee[i]] = true; + c.memberStatus[c.topNodes[i]] = ICiphernodeRegistry + .MemberStatus + .Active; } c.activeCount = committeeLen; @@ -464,7 +487,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @return Whether the submission window is open function isOpen(uint256 e3Id) public view returns (bool) { Committee storage c = committees[e3Id]; - if (!c.initialized || c.finalized) return false; + if (c.stage != ICiphernodeRegistry.CommitteeStage.Requested) + return false; return block.timestamp <= c.committeeDeadline; } @@ -511,7 +535,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { ) public view returns (address[] memory nodes) { Committee storage c = committees[e3Id]; require(c.publicKey != bytes32(0), CommitteeNotPublished()); - nodes = c.committee; + nodes = c.topNodes; } /// @notice Returns the current size of the ciphernode IMT @@ -531,7 +555,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint256 e3Id ) external view returns (uint256) { Committee storage c = committees[e3Id]; - require(c.initialized, CommitteeNotRequested()); + require( + c.stage != ICiphernodeRegistry.CommitteeStage.None, + CommitteeNotRequested() + ); return c.committeeDeadline; } @@ -552,16 +579,19 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { returns (uint256 activeCount, uint32 thresholdM) { Committee storage c = committees[e3Id]; - require(c.finalized, CommitteeNotFinalized()); + require( + c.stage == ICiphernodeRegistry.CommitteeStage.Finalized, + CommitteeNotFinalized() + ); thresholdM = c.threshold[0]; - // Idempotent: if already expelled, return current state - if (!c.active[node]) { + // Idempotent: if already expelled (or never a member), return current state + if (c.memberStatus[node] != ICiphernodeRegistry.MemberStatus.Active) { activeCount = c.activeCount; return (activeCount, thresholdM); } - c.active[node] = false; + c.memberStatus[node] = ICiphernodeRegistry.MemberStatus.Expelled; c.activeCount--; activeCount = c.activeCount; @@ -577,7 +607,19 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint256 e3Id, address node ) external view returns (bool) { - return committees[e3Id].active[node]; + return + committees[e3Id].memberStatus[node] == + ICiphernodeRegistry.MemberStatus.Active; + } + + /// @inheritdoc ICiphernodeRegistry + function isCommitteeMember( + uint256 e3Id, + address node + ) external view returns (bool) { + return + committees[e3Id].memberStatus[node] != + ICiphernodeRegistry.MemberStatus.None; } /// @inheritdoc ICiphernodeRegistry @@ -585,14 +627,17 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint256 e3Id ) external view returns (address[] memory) { Committee storage c = committees[e3Id]; - uint256 total = c.committee.length; + uint256 total = c.topNodes.length; uint256 actCount = c.activeCount; address[] memory activeNodes = new address[](actCount); uint256 idx = 0; for (uint256 i = 0; i < total; ++i) { - if (c.active[c.committee[i]]) { - activeNodes[idx] = c.committee[i]; + if ( + c.memberStatus[c.topNodes[i]] == + ICiphernodeRegistry.MemberStatus.Active + ) { + activeNodes[idx] = c.topNodes[i]; idx++; } } @@ -600,17 +645,23 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @inheritdoc ICiphernodeRegistry - function getActiveCommitteeCount( - uint256 e3Id - ) external view returns (uint256) { - return committees[e3Id].activeCount; - } - - /// @inheritdoc ICiphernodeRegistry - function getCommitteeThreshold( + function getCommitteeViability( uint256 e3Id - ) external view returns (uint32[2] memory) { - return committees[e3Id].threshold; + ) + external + view + returns ( + uint256 activeCount, + uint32 thresholdM, + uint32 thresholdN, + bool viable + ) + { + Committee storage c = committees[e3Id]; + activeCount = c.activeCount; + thresholdM = c.threshold[0]; + thresholdN = c.threshold[1]; + viable = activeCount >= thresholdM; } //////////////////////////////////////////////////////////// diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 0f26369e72..5f032fd361 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -423,9 +423,15 @@ contract SlashingManager is ISlashingManager, AccessControl { address recoveredSigner = ECDSA.recover(ethSignedHash, signature); require(recoveredSigner == operator, SignerIsNotOperator()); - // 3. Verify committee membership. + // 3. Verify the operator was ever a committee member for this E3. + // We use isCommitteeMember (permanent, never cleared) rather than + // isCommitteeMemberActive (cleared on expulsion), so that a second + // provable fault by the same already-expelled operator can still be + // penalized via Lane A. The first slash already triggered expulsion + // and (if threshold dropped below M) E3 failure — the financial + // penalty for subsequent faults is still a valid deterrent. require( - ciphernodeRegistry.isCommitteeMemberActive(e3Id, operator), + ciphernodeRegistry.isCommitteeMember(e3Id, operator), OperatorNotInCommittee() ); diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index a16cdc38cf..6f1d3a4d10 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -118,10 +118,25 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } function isCommitteeMemberActive( - uint256, - address - ) external pure returns (bool) { - return true; + uint256 e3Id, + address node + ) external view returns (bool) { + address[] storage nodes = _committeeNodes[e3Id]; + for (uint256 i = 0; i < nodes.length; i++) { + if (nodes[i] == node) return true; + } + return false; + } + + function isCommitteeMember( + uint256 e3Id, + address node + ) external view returns (bool) { + address[] storage nodes = _committeeNodes[e3Id]; + for (uint256 i = 0; i < nodes.length; i++) { + if (nodes[i] == node) return true; + } + return false; } function getActiveCommitteeNodes( @@ -130,14 +145,10 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { return new address[](0); } - function getActiveCommitteeCount(uint256) external pure returns (uint256) { - return 0; - } - - function getCommitteeThreshold( + function getCommitteeViability( uint256 - ) external pure returns (uint32[2] memory) { - return [uint32(0), uint32(0)]; + ) external pure returns (uint256, uint32, uint32, bool) { + return (0, 0, 0, false); } } @@ -238,7 +249,11 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { uint256, address ) external pure returns (bool) { - return true; + return false; + } + + function isCommitteeMember(uint256, address) external pure returns (bool) { + return false; } function getActiveCommitteeNodes( @@ -247,13 +262,9 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { return new address[](0); } - function getActiveCommitteeCount(uint256) external pure returns (uint256) { - return 0; - } - - function getCommitteeThreshold( + function getCommitteeViability( uint256 - ) external pure returns (uint32[2] memory) { - return [uint32(0), uint32(0)]; + ) external pure returns (uint256, uint32, uint32, bool) { + return (0, 0, 0, false); } } diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts index 7bcea8071d..2e04cc9a7e 100644 --- a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -460,7 +460,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { // Verify member is active before slash expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.true; - expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(3); // Submit slash proposal — MockCircuitVerifier returns false by default // so fault is confirmed and slash is auto-executed @@ -488,7 +488,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { // Verify member is no longer active expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.false; - expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(2); }); it("should keep E3 alive when active members >= threshold", async function () { @@ -535,9 +535,10 @@ describe("Committee Expulsion & Fault Tolerance", function () { expect(stage).to.not.equal(6); // 6 = E3Stage.Failed // Active committee still has enough members - expect(await registry.getActiveCommitteeCount(0)).to.equal(2); - const threshold = await registry.getCommitteeThreshold(0); - expect(threshold[0]).to.equal(2); // M=2 + const { activeCount, thresholdM } = + await registry.getCommitteeViability(0); + expect(activeCount).to.equal(2); + expect(thresholdM).to.equal(2); // M=2 }); it("should fail E3 when active members drop below threshold", async function () { @@ -644,7 +645,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { REASON_BAD_DKG, proof1, ); - expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(2); // Slash operator1 again with different proof (different evidence key) const proof2 = await signAndEncodeProof( @@ -661,7 +662,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { ); // Active count should still be 2 (idempotent expulsion) - expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(2); }); it("should exclude expelled members from getActiveCommitteeNodes", async function () { @@ -744,7 +745,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { operator4, ]); - expect(await registry.getActiveCommitteeCount(0)).to.equal(4); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(4); // Expel 2 out of 4 — still have 2 >= M=2 const proof1 = await signAndEncodeProof( @@ -759,7 +760,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { REASON_BAD_DKG, proof1, ); - expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(3); const proof2 = await signAndEncodeProof( operator2, @@ -773,7 +774,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { REASON_BAD_DKG, proof2, ); - expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + expect((await registry.getCommitteeViability(0)).activeCount).to.equal(2); // E3 should NOT be failed const stage = await enclave.getE3Stage(0); From e60223242f92d483a5e741aa9d617a941f67bbb3 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 24 Feb 2026 02:01:15 +0500 Subject: [PATCH 10/21] fix: contract tests --- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 2 +- .../ISlashingManager.json | 2 +- .../contracts/interfaces/IEnclave.sol | 2 - .../registry/CiphernodeRegistryOwnable.sol | 3 - .../contracts/slashing/SlashingManager.sol | 24 +- .../ignition/modules/ciphernodeRegistry.ts | 2 - .../ignition/modules/slashingManager.ts | 10 +- .../ciphernodeRegistryOwnable.ts | 9 +- .../scripts/deployAndSave/slashingManager.ts | 28 +- .../scripts/deployEnclave.ts | 32 +-- .../test/E3Lifecycle/E3Integration.spec.ts | 159 +++++------- .../enclave-contracts/test/Enclave.spec.ts | 245 ++++++++---------- .../test/Registry/BondingRegistry.spec.ts | 105 +++----- .../CiphernodeRegistryOwnable.spec.ts | 196 ++++++-------- .../test/Slashing/CommitteeExpulsion.spec.ts | 186 ++++++------- .../test/Slashing/SlashingManager.spec.ts | 69 +++-- 18 files changed, 420 insertions(+), 658 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index dfdc8aa85b..dca24c8413 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -922,5 +922,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" + "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 8e63f27784..12c2ba2ced 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -787,5 +787,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" + "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index d500ba38ba..fe3403cb14 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -1226,5 +1226,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" + "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json index 1744600bb4..3ca8cb1cf7 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -865,5 +865,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", - "buildInfoId": "solc-0_8_28-b0b980b40f181abc37acd0cc4334feb6b35db41a" + "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 758202a906..f4d65d5a42 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -23,8 +23,6 @@ interface IEnclave { None, Requested, CommitteeFinalized, - // Once a key is published, it is possible to then accept inputs - // as long as we are within the input deadline (start and end) KeyPublished, CiphertextReady, Complete, diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index bd2a5bbff6..ebbe05eef0 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -204,17 +204,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Initializes the registry contract /// @dev Can only be called once due to initializer modifier /// @param _owner Address that will own the contract - /// @param _enclave Address of the Enclave contract /// @param _submissionWindow The submission window for the E3 sortition in seconds function initialize( address _owner, - IEnclave _enclave, uint256 _submissionWindow ) public initializer { require(_owner != address(0), ZeroAddress()); __Ownable_init(msg.sender); - setEnclave(_enclave); setSortitionSubmissionWindow(_submissionWindow); if (_owner != owner()) transferOwnership(_owner); } diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 5f032fd361..f392d6f4ca 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -103,25 +103,9 @@ contract SlashingManager is ISlashingManager, AccessControl { /** * @notice Initializes the SlashingManager contract * @param admin Address to receive DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE - * @param _bondingRegistry Address of the bonding registry contract - * @param _ciphernodeRegistry Address of the ciphernode registry contract - * @param _enclave Address of the Enclave contract */ - constructor( - address admin, - address _bondingRegistry, - address _ciphernodeRegistry, - address _enclave - ) { + constructor(address admin) { require(admin != address(0), ZeroAddress()); - require(_bondingRegistry != address(0), ZeroAddress()); - require(_ciphernodeRegistry != address(0), ZeroAddress()); - require(_enclave != address(0), ZeroAddress()); - - bondingRegistry = IBondingRegistry(_bondingRegistry); - ciphernodeRegistry = ICiphernodeRegistry(_ciphernodeRegistry); - enclave = IEnclave(_enclave); - _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(GOVERNANCE_ROLE, admin); } @@ -424,12 +408,6 @@ contract SlashingManager is ISlashingManager, AccessControl { require(recoveredSigner == operator, SignerIsNotOperator()); // 3. Verify the operator was ever a committee member for this E3. - // We use isCommitteeMember (permanent, never cleared) rather than - // isCommitteeMemberActive (cleared on expulsion), so that a second - // provable fault by the same already-expelled operator can still be - // penalized via Lane A. The first slash already triggered expulsion - // and (if threshold dropped below M) E3 failure — the financial - // penalty for subsequent faults is still a valid deterrent. require( ciphernodeRegistry.isCommitteeMember(e3Id, operator), OperatorNotInCommittee() diff --git a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts index eb2d73019b..49ca18adfc 100644 --- a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts +++ b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts @@ -6,7 +6,6 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("CiphernodeRegistry", (m) => { - const enclaveAddress = m.getParameter("enclaveAddress"); const owner = m.getParameter("owner"); const submissionWindow = m.getParameter("submissionWindow"); @@ -20,7 +19,6 @@ export default buildModule("CiphernodeRegistry", (m) => { const initData = m.encodeFunctionCall(cipherNodeRegistryImpl, "initialize", [ owner, - enclaveAddress, submissionWindow, ]); diff --git a/packages/enclave-contracts/ignition/modules/slashingManager.ts b/packages/enclave-contracts/ignition/modules/slashingManager.ts index e44ad80287..44f1aa2f57 100644 --- a/packages/enclave-contracts/ignition/modules/slashingManager.ts +++ b/packages/enclave-contracts/ignition/modules/slashingManager.ts @@ -6,17 +6,9 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("SlashingManager", (m) => { - const bondingRegistry = m.getParameter("bondingRegistry"); - const ciphernodeRegistry = m.getParameter("ciphernodeRegistry"); - const enclave = m.getParameter("enclave"); const admin = m.getParameter("admin"); - const slashingManager = m.contract("SlashingManager", [ - admin, - bondingRegistry, - ciphernodeRegistry, - enclave, - ]); + const slashingManager = m.contract("SlashingManager", [admin]); return { slashingManager }; }) as any; diff --git a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts index dcdce4c5d8..746d298000 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts @@ -16,7 +16,6 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; * The arguments for the deployAndSaveCiphernodeRegistryOwnable function */ export interface CiphernodeRegistryOwnableArgs { - enclaveAddress?: string; owner?: string; submissionWindow?: number; poseidonT3Address: string; @@ -29,7 +28,6 @@ export interface CiphernodeRegistryOwnableArgs { * @returns The deployed CiphernodeRegistryOwnable contract */ export const deployAndSaveCiphernodeRegistryOwnable = async ({ - enclaveAddress, owner, submissionWindow, poseidonT3Address, @@ -47,11 +45,9 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ ); if ( - !enclaveAddress || !owner || !submissionWindow || - (preDeployedArgs?.constructorArgs?.enclaveAddress === enclaveAddress && - preDeployedArgs?.constructorArgs?.owner === owner && + (preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.submissionWindow === submissionWindow.toString()) ) { @@ -83,7 +79,7 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ const initData = ciphernodeRegistryFactory.interface.encodeFunctionData( "initialize", - [owner, enclaveAddress, submissionWindow], + [owner, submissionWindow], ); const ProxyCF = await ethers.getContractFactory( @@ -103,7 +99,6 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ { constructorArgs: { owner, - enclaveAddress: enclaveAddress, submissionWindow: submissionWindow.toString(), }, proxyRecords: { diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts index fdd28469b8..11292961b4 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -16,9 +16,6 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; */ export interface SlashingManagerArgs { admin?: string; - bondingRegistry?: string; - ciphernodeRegistry?: string; - enclave?: string; hre: HardhatRuntimeEnvironment; } @@ -29,9 +26,6 @@ export interface SlashingManagerArgs { */ export const deployAndSaveSlashingManager = async ({ admin, - bondingRegistry, - ciphernodeRegistry, - enclave, hre, }: SlashingManagerArgs): Promise<{ slashingManager: SlashingManager; @@ -42,17 +36,7 @@ export const deployAndSaveSlashingManager = async ({ const preDeployedArgs = readDeploymentArgs("SlashingManager", chain); - if ( - !admin || - !bondingRegistry || - !ciphernodeRegistry || - !enclave || - (preDeployedArgs?.constructorArgs?.admin === admin && - preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && - preDeployedArgs?.constructorArgs?.ciphernodeRegistry === - ciphernodeRegistry && - preDeployedArgs?.constructorArgs?.enclave === enclave) - ) { + if (!admin || preDeployedArgs?.constructorArgs?.admin === admin) { if (!preDeployedArgs?.address) { throw new Error( "SlashingManager address not found, it must be deployed first", @@ -67,12 +51,7 @@ export const deployAndSaveSlashingManager = async ({ const slashingManagerFactory = await ethers.getContractFactory("SlashingManager"); - const slashingManager = await slashingManagerFactory.deploy( - admin, - bondingRegistry, - ciphernodeRegistry, - enclave, - ); + const slashingManager = await slashingManagerFactory.deploy(admin); await slashingManager.waitForDeployment(); @@ -84,9 +63,6 @@ export const deployAndSaveSlashingManager = async ({ { constructorArgs: { admin, - bondingRegistry, - ciphernodeRegistry, - enclave, }, blockNumber, address: slashingManagerAddress, diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 10409bc55e..effca3bf47 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -100,20 +100,27 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Deploying SlashingManager..."); const { slashingManager } = await deployAndSaveSlashingManager({ admin: ownerAddress, - bondingRegistry: addressOne, - ciphernodeRegistry: addressOne, - enclave: addressOne, hre, }); const slashingManagerAddress = await slashingManager.getAddress(); console.log("SlashingManager deployed to:", slashingManagerAddress); + console.log("Deploying CiphernodeRegistry..."); + const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ + poseidonT3Address: poseidonT3, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + hre, + }); + const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); + console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); + console.log("Deploying BondingRegistry..."); const { bondingRegistry } = await deployAndSaveBondingRegistry({ owner: ownerAddress, ticketToken: enclaveTicketTokenAddress, licenseToken: enclaveTokenAddress, - registry: addressOne, + registry: ciphernodeRegistryAddress, slashedFundsTreasury: ownerAddress, ticketPrice: ethers.parseUnits("10", 6).toString(), licenseRequiredBond: ethers.parseEther("100").toString(), @@ -124,17 +131,6 @@ export const deployEnclave = async (withMocks?: boolean) => { const bondingRegistryAddress = await bondingRegistry.getAddress(); console.log("BondingRegistry deployed to:", bondingRegistryAddress); - console.log("Deploying CiphernodeRegistry..."); - const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ - poseidonT3Address: poseidonT3, - enclaveAddress: addressOne, - owner: ownerAddress, - submissionWindow: SORTITION_SUBMISSION_WINDOW, - hre, - }); - const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); - console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); - console.log("Deploying Enclave..."); const { enclave } = await deployAndSaveEnclave({ params: [encoded], @@ -187,15 +183,15 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting CiphernodeRegistry address in BondingRegistry..."); await bondingRegistry.setRegistry(ciphernodeRegistryAddress); + console.log("Setting Enclave address in SlashingManager..."); + await slashingManager.setEnclave(enclaveAddress); + console.log("Setting BondingRegistry address in SlashingManager..."); await slashingManager.setBondingRegistry(bondingRegistryAddress); console.log("Setting CiphernodeRegistry address in SlashingManager..."); await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); - console.log("Setting Enclave address in SlashingManager..."); - await slashingManager.setEnclave(enclaveAddress); - console.log("Setting SlashingManager address in Enclave..."); await enclave.setSlashingManager(slashingManagerAddress); diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index e42635d1cb..79db3af244 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -70,8 +70,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const encryptionSchemeId = "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; - const setup = async () => { + // ── Signers ──────────────────────────────────────────────────────────────── const [owner, requester, treasury, operator1, operator2, computeProvider] = await ethers.getSigners(); @@ -79,34 +79,24 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const treasuryAddress = await treasury.getAddress(); const requesterAddress = await requester.getAddress(); - // Deploy USDC mock - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 10000000, - }, - }, + // ── Token Contracts ──────────────────────────────────────────────────────── + const { mockUSDC } = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 10_000_000 } }, }); const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), + await mockUSDC.getAddress(), owner, ); - // Deploy ENCL token - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, - }, + const { enclaveToken } = await ignition.deploy(EnclaveTokenModule, { + parameters: { EnclaveToken: { owner: ownerAddress } }, }); const enclToken = EnclaveTokenFactory.connect( - await enclTokenContract.enclaveToken.getAddress(), + await enclaveToken.getAddress(), owner, ); - // Deploy ticket token - const ticketTokenContract = await ignition.deploy( + const { enclaveTicketToken } = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { @@ -119,32 +109,41 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }, ); - // Deploy slashing manager - const slashingManagerContract = await ignition.deploy( - SlashingManagerModule, + // ── Registry & Slashing ──────────────────────────────────────────────────── + const { slashingManager } = await ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin: ownerAddress, + }, + }, + }); + + const { cipherNodeRegistry } = await ignition.deploy( + CiphernodeRegistryModule, { parameters: { - SlashingManager: { - admin: ownerAddress, - bondingRegistry: addressOne, // Will be updated - ciphernodeRegistry: addressOne, // Will be updated - enclave: addressOne, // Will be updated + CiphernodeRegistry: { + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, }, }, }, ); + const ciphernodeRegistryAddress = await cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryOwnableFactory.connect( + ciphernodeRegistryAddress, + owner, + ); - // Deploy bonding registry - const bondingRegistryContract = await ignition.deploy( + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), + ticketToken: await enclaveTicketToken.getAddress(), licenseToken: await enclToken.getAddress(), - registry: addressOne, // Will be updated + registry: ciphernodeRegistryAddress, slashedFundsTreasury: treasuryAddress, ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), @@ -155,47 +154,29 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }, ); const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), + await _bondingRegistry.getAddress(), owner, ); - // Deploy Enclave (with addressOne as temp registry) - const enclaveContract = await ignition.deploy(EnclaveModule, { + // ── Enclave ──────────────────────────────────────────────────────────────── + const { enclave: _enclave } = await ignition.deploy(EnclaveModule, { parameters: { Enclave: { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: THIRTY_DAYS, - registry: addressOne, - e3RefundManager: addressOne, + registry: ciphernodeRegistryAddress, bondingRegistry: await bondingRegistry.getAddress(), + e3RefundManager: addressOne, // updated below feeToken: await usdcToken.getAddress(), timeoutConfig: defaultTimeoutConfig, }, }, }); - const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclaveAddress = await _enclave.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - // Deploy CiphernodeRegistry - const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { - parameters: { - CiphernodeRegistry: { - enclaveAddress: enclaveAddress, - owner: ownerAddress, - submissionWindow: SORTITION_SUBMISSION_WINDOW, - }, - }, - }); - const ciphernodeRegistryAddress = - await ciphernodeRegistry.cipherNodeRegistry.getAddress(); - const registry = CiphernodeRegistryOwnableFactory.connect( - ciphernodeRegistryAddress, - owner, - ); - - // Deploy E3RefundManager - const e3RefundManagerContract = await ignition.deploy( + const { e3RefundManager: _e3RefundManager } = await ignition.deploy( E3RefundManagerModule, { parameters: { @@ -207,37 +188,30 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }, }, ); - const e3RefundManagerAddress = - await e3RefundManagerContract.e3RefundManager.getAddress(); + const e3RefundManagerAddress = await _e3RefundManager.getAddress(); const e3RefundManager = E3RefundManagerFactory.connect( e3RefundManagerAddress, owner, ); - // Deploy mock E3 Program - const e3ProgramContract = await ignition.deploy(MockE3ProgramModule, { - parameters: { - MockE3Program: { - encryptionSchemeId: encryptionSchemeId, - }, - }, + // ── Mock E3 Program & Decryption Verifier ────────────────────────────────── + const { mockE3Program } = await ignition.deploy(MockE3ProgramModule, { + parameters: { MockE3Program: { encryptionSchemeId } }, }); const e3Program = MockE3ProgramFactory.connect( - await e3ProgramContract.mockE3Program.getAddress(), + await mockE3Program.getAddress(), owner, ); - // Deploy mock decryption verifier - const decryptionVerifierContract = await ignition.deploy( + const { mockDecryptionVerifier } = await ignition.deploy( MockDecryptionVerifierModule, ); const decryptionVerifier = MockDecryptionVerifierFactory.connect( - await decryptionVerifierContract.mockDecryptionVerifier.getAddress(), + await mockDecryptionVerifier.getAddress(), owner, ); - // Wire up all the contracts - await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); + // ── Wire Up Contracts ────────────────────────────────────────────────────── await enclave.setE3RefundManager(e3RefundManagerAddress); await enclave.enableE3Program(await e3Program.getAddress()); await enclave.setDecryptionVerifier( @@ -245,35 +219,28 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await decryptionVerifier.getAddress(), ); - // Setup bonding registry connections await bondingRegistry.setRewardDistributor(enclaveAddress); - await bondingRegistry.setRegistry(ciphernodeRegistryAddress); await bondingRegistry.setSlashingManager( - await slashingManagerContract.slashingManager.getAddress(), + await slashingManager.getAddress(), ); - await slashingManagerContract.slashingManager.setBondingRegistry( + + await slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), ); - await slashingManagerContract.slashingManager.setCiphernodeRegistry( - ciphernodeRegistryAddress, - ); - await slashingManagerContract.slashingManager.setEnclave(enclaveAddress); + await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); + await slashingManager.setEnclave(enclaveAddress); + + await registry.setEnclave(enclaveAddress); await registry.setBondingRegistry(await bondingRegistry.getAddress()); - await registry.setSlashingManager( - await slashingManagerContract.slashingManager.getAddress(), - ); + await registry.setSlashingManager(await slashingManager.getAddress()); - // Update ticket token registry - await ticketTokenContract.enclaveTicketToken.setRegistry( - await bondingRegistry.getAddress(), - ); + await enclaveTicketToken.setRegistry(await bondingRegistry.getAddress()); - // Mint tokens to requester + // ── Mint Tokens ──────────────────────────────────────────────────────────── await usdcToken.mint(requesterAddress, ethers.parseUnits("10000", 6)); - // Mint tokens to refund manager for distribution tests await usdcToken.mint(e3RefundManagerAddress, ethers.parseUnits("10000", 6)); - // Helper to make E3 request + // ── Helpers ──────────────────────────────────────────────────────────────── const makeRequest = async ( signer: Signer = requester, ): Promise<{ e3Id: number }> => { @@ -284,7 +251,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { inputWindow: [startTime + 100, startTime + ONE_DAY] as [number, number], e3Program: await e3Program.getAddress(), e3ProgramParams: encodedE3ProgramParams, - // computeProviderParams must be exactly 32 bytes for MockE3Program.validate computeProviderParams: abiCoder.encode( ["address"], [await decryptionVerifier.getAddress()], @@ -297,15 +263,15 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const fee = await enclave.getE3Quote(requestParams); await usdcToken.connect(signer).approve(enclaveAddress, fee); - await enclave.connect(signer).request(requestParams); - // Get e3Id from event (it's 0 for first request) return { e3Id: 0 }; }; - async function setupOperator(operator: Signer) { + const setupOperator = async (operator: Signer) => { const operatorAddress = await operator.getAddress(); + const ticketTokenAddress = await bondingRegistry.ticketToken(); + const ticketAmount = ethers.parseUnits("100", 6); await enclToken.setTransferRestriction(false); await enclToken.mintAllocation( @@ -323,14 +289,13 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { .bondLicense(ethers.parseEther("1000")); await bondingRegistry.connect(operator).registerOperator(); - const ticketTokenAddress = await bondingRegistry.ticketToken(); - const ticketAmount = ethers.parseUnits("100", 6); await usdcToken .connect(operator) .approve(ticketTokenAddress, ticketAmount); await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); - } + }; + // ── Return ───────────────────────────────────────────────────────────────── return { enclave, e3RefundManager, diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 54969c83e6..ec5ef122cf 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -138,33 +138,28 @@ describe("Enclave", function () { } const setup = async () => { + // ── Signers ────────────────────────────────────────────────────────────── const [owner, notTheOwner, operator1, operator2] = await ethers.getSigners(); - const ownerAddress = await owner.getAddress(); - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 1000000, - }, - }, + // ── Token Contracts ─────────────────────────────────────────────────────── + const { mockUSDC } = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 1_000_000 } }, }); - const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), + await mockUSDC.getAddress(), owner, ); - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, + const { enclaveToken: licenseToken } = await ignition.deploy( + EnclaveTokenModule, + { + parameters: { EnclaveToken: { owner: ownerAddress } }, }, - }); + ); - const ticketTokenContract = await ignition.deploy( + const { enclaveTicketToken: ticketToken } = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { @@ -177,30 +172,41 @@ describe("Enclave", function () { }, ); - const slashingManagerContract = await ignition.deploy( - SlashingManagerModule, + // ── Registry & Slashing ─────────────────────────────────────────────────── + const { slashingManager } = await ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin: ownerAddress, + }, + }, + }); + + const { cipherNodeRegistry } = await ignition.deploy( + CiphernodeRegistryModule, { parameters: { - SlashingManager: { - admin: ownerAddress, - bondingRegistry: addressOne, - ciphernodeRegistry: addressOne, - enclave: addressOne, + CiphernodeRegistry: { + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, }, }, }, ); + const ciphernodeRegistryAddress = await cipherNodeRegistry.getAddress(); + const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( + ciphernodeRegistryAddress, + owner, + ); - const bondingRegistryContract = await ignition.deploy( + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), - licenseToken: await enclTokenContract.enclaveToken.getAddress(), - registry: addressOne, + ticketToken: await ticketToken.getAddress(), + licenseToken: await licenseToken.getAddress(), + registry: ciphernodeRegistryAddress, slashedFundsTreasury: ownerAddress, ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), @@ -210,179 +216,130 @@ describe("Enclave", function () { }, }, ); + const bondingRegistry = BondingRegistryFactory.connect( + await _bondingRegistry.getAddress(), + owner, + ); - const enclaveContract = await ignition.deploy(EnclaveModule, { + // ── Enclave ─────────────────────────────────────────────────────────────── + const { enclave: _enclave } = await ignition.deploy(EnclaveModule, { parameters: { Enclave: { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: THIRTY_DAYS_IN_SECONDS, - registry: addressOne, - bondingRegistry: - await bondingRegistryContract.bondingRegistry.getAddress(), - e3RefundManager: addressOne, // placeholder, will be updated + registry: ciphernodeRegistryAddress, + bondingRegistry: await bondingRegistry.getAddress(), + e3RefundManager: addressOne, // placeholder — updated below feeToken: await usdcToken.getAddress(), timeoutConfig, }, }, }); - - const enclaveAddress = await enclaveContract.enclave.getAddress(); - - const e3RefundManagerContract = await ignition.deploy( - E3RefundManagerModule, - { - parameters: { - E3RefundManager: { - owner: ownerAddress, - enclave: enclaveAddress, - treasury: ownerAddress, - }, - }, - }, - ); - - const e3RefundManagerAddress = - await e3RefundManagerContract.e3RefundManager.getAddress(); - + const enclaveAddress = await _enclave.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - await enclave.setE3RefundManager(e3RefundManagerAddress); - const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { + const { e3RefundManager } = await ignition.deploy(E3RefundManagerModule, { parameters: { - CiphernodeRegistry: { - enclaveAddress: enclaveAddress, + E3RefundManager: { owner: ownerAddress, - submissionWindow: SORTITION_SUBMISSION_WINDOW, + enclave: enclaveAddress, + treasury: ownerAddress, }, }, }); + await enclave.setE3RefundManager(await e3RefundManager.getAddress()); - const ciphernodeRegistryAddress = - await ciphernodeRegistry.cipherNodeRegistry.getAddress(); - - const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( - ciphernodeRegistryAddress, - owner, - ); - - const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), - owner, - ); - + // ── Wire Up Contracts ───────────────────────────────────────────────────── const registryAddress = await enclave.ciphernodeRegistry(); - if (registryAddress !== ciphernodeRegistryAddress) { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } + await ciphernodeRegistryContract.setEnclave(enclaveAddress); await ciphernodeRegistryContract.setBondingRegistry( await bondingRegistry.getAddress(), ); - - await ticketTokenContract.enclaveTicketToken.setRegistry( - await bondingRegistry.getAddress(), - ); - await bondingRegistry.setRegistry(ciphernodeRegistryAddress); + await ticketToken.setRegistry(await bondingRegistry.getAddress()); await bondingRegistry.setSlashingManager( - await slashingManagerContract.slashingManager.getAddress(), - ); - await slashingManagerContract.slashingManager.setBondingRegistry( - await bondingRegistry.getAddress(), + await slashingManager.getAddress(), ); - await bondingRegistry.setRewardDistributor(enclaveAddress); - - const tree = new LeanIMT(hash); - - const licenseToken = enclTokenContract.enclaveToken; - const ticketToken = ticketTokenContract.enclaveTicketToken; - - await licenseToken.setTransferRestriction(false); - - await setupOperatorForSortition( - operator1, - bondingRegistry, - licenseToken, - usdcToken, - ticketToken, - ciphernodeRegistryContract, - ); - tree.insert(BigInt(await operator1.getAddress())); - - await setupOperatorForSortition( - operator2, - bondingRegistry, - licenseToken, - usdcToken, - ticketToken, - ciphernodeRegistryContract, + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), ); - tree.insert(BigInt(await operator2.getAddress())); - - await mine(1); - const mockComputeProvider = await ignition.deploy( + // ── Mocks ───────────────────────────────────────────────────────────────── + const { mockComputeProvider } = await ignition.deploy( mockComputeProviderModule, ); + const { mockDecryptionVerifier: decryptionVerifier } = + await ignition.deploy(MockDecryptionVerifierModule); + const { mockE3Program: e3Program } = + await ignition.deploy(MockE3ProgramModule); - const decryptionVerifier = await ignition.deploy( - MockDecryptionVerifierModule, - ); - - const e3Program = await ignition.deploy(MockE3ProgramModule); - - await enclave.enableE3Program(await e3Program.mockE3Program.getAddress()); + await enclave.enableE3Program(await e3Program.getAddress()); await enclave.setE3ProgramsParams([encodedE3ProgramParams]); await enclave.setDecryptionVerifier( encryptionSchemeId, - await decryptionVerifier.mockDecryptionVerifier.getAddress(), + await decryptionVerifier.getAddress(), ); + // ── Operators ───────────────────────────────────────────────────────────── + await licenseToken.setTransferRestriction(false); + const tree = new LeanIMT(hash); + + for (const operator of [operator1, operator2]) { + await setupOperatorForSortition( + operator, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + ciphernodeRegistryContract, + ); + tree.insert(BigInt(await operator.getAddress())); + } + await mine(1); + + // ── Mint USDC ───────────────────────────────────────────────────────────── + const mintAmount = ethers.parseUnits("1000000", 6); + await usdcToken.mint(ownerAddress, mintAmount); + await usdcToken.mint(await notTheOwner.getAddress(), mintAmount); + + // ── Request ─────────────────────────────────────────────────────────────── + const now = await time.latest(); const request = { threshold: [2, 2] as [number, number], - inputWindow: [ - (await time.latest()) + 10, - (await time.latest()) + inputWindowDuration, - ] as [number, number], - e3Program: await e3Program.mockE3Program.getAddress(), + inputWindow: [now + 10, now + inputWindowDuration] as [number, number], + e3Program: await e3Program.getAddress(), e3ProgramParams: encodedE3ProgramParams, computeProviderParams: abiCoder.encode( ["address"], - [await decryptionVerifier.mockDecryptionVerifier.getAddress()], + [await decryptionVerifier.getAddress()], ), customParams: abiCoder.encode( ["address"], - ["0x1234567890123456789012345678901234567890"], // arbitrary address. + ["0x1234567890123456789012345678901234567890"], ), }; - await usdcToken.mint(ownerAddress, ethers.parseUnits("1000000", 6)); - await usdcToken.mint( - await notTheOwner.getAddress(), - ethers.parseUnits("1000000", 6), - ); - + // ── Return ──────────────────────────────────────────────────────────────── return { + owner, + notTheOwner, + operator1, + operator2, enclave, ciphernodeRegistryContract, - bondingRegistry: bondingRegistry, - ticketToken: ticketTokenContract.enclaveTicketToken, - licenseToken: licenseToken, + bondingRegistry, + licenseToken, + ticketToken, usdcToken, - slashingManager: slashingManagerContract.slashingManager, + slashingManager, tree, - mocks: { - decryptionVerifier: decryptionVerifier.mockDecryptionVerifier, - e3Program: e3Program.mockE3Program, - mockComputeProvider: mockComputeProvider.mockComputeProvider, - }, request, - owner, - notTheOwner, - operator1, - operator2, + mocks: { decryptionVerifier, e3Program, mockComputeProvider }, }; }; diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index 479f1e7269..103aa14996 100644 --- a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -41,80 +41,65 @@ describe("BondingRegistry", function () { const LICENSE_REQUIRED_BOND = ethers.parseEther("1000"); const MIN_TICKET_BALANCE = 5; async function setup() { + // ── Signers ──────────────────────────────────────────────────────────────── const [owner, operator1, operator2, treasury, notTheOwner] = await ethers.getSigners(); - const ownerAddress = await owner.getAddress(); const operator1Address = await operator1.getAddress(); const operator2Address = await operator2.getAddress(); const treasuryAddress = await treasury.getAddress(); - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 1000000, - }, - }, + // ── Token Contracts ──────────────────────────────────────────────────────── + const { mockUSDC } = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 1_000_000 } }, }); - - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, - }, + const { enclaveToken } = await ignition.deploy(EnclaveTokenModule, { + parameters: { EnclaveToken: { owner: ownerAddress } }, }); - - const ciphernodeRegistryContract = await ignition.deploy( - MockCiphernodeRegistryModule, + const { enclaveTicketToken } = await ignition.deploy( + EnclaveTicketTokenModule, { parameters: { - CiphernodeRegistry: { - enclaveAddress: ownerAddress, + EnclaveTicketToken: { + baseToken: await mockUSDC.getAddress(), + registry: AddressOne, owner: ownerAddress, }, }, }, ); - const ticketTokenContract = await ignition.deploy( - EnclaveTicketTokenModule, + // ── Registry & Slashing ──────────────────────────────────────────────────── + const { mockCiphernodeRegistry } = await ignition.deploy( + MockCiphernodeRegistryModule, { parameters: { - EnclaveTicketToken: { - baseToken: await usdcContract.mockUSDC.getAddress(), - registry: AddressOne, + CiphernodeRegistry: { + enclaveAddress: ownerAddress, owner: ownerAddress, }, }, }, ); - - const slashingManagerContract = await ignition.deploy( + const { slashingManager: _slashingManager } = await ignition.deploy( SlashingManagerModule, { parameters: { SlashingManager: { admin: ownerAddress, - bondingRegistry: AddressOne, - ciphernodeRegistry: AddressOne, - enclave: AddressOne, }, }, }, ); - - const bondingRegistryContract = await ignition.deploy( + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), - licenseToken: await enclTokenContract.enclaveToken.getAddress(), - registry: - await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + ticketToken: await enclaveTicketToken.getAddress(), + licenseToken: await enclaveToken.getAddress(), + registry: await mockCiphernodeRegistry.getAddress(), slashedFundsTreasury: treasuryAddress, ticketPrice: TICKET_PRICE, licenseRequiredBond: LICENSE_REQUIRED_BOND, @@ -125,31 +110,33 @@ describe("BondingRegistry", function () { }, ); + // ── Typed Contract Instances ─────────────────────────────────────────────── const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), + await _bondingRegistry.getAddress(), owner, ); const ticketToken = EnclaveTicketTokenFactory.connect( - await ticketTokenContract.enclaveTicketToken.getAddress(), + await enclaveTicketToken.getAddress(), owner, ); const licenseToken = EnclaveTokenFactory.connect( - await enclTokenContract.enclaveToken.getAddress(), + await enclaveToken.getAddress(), owner, ); const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), + await mockUSDC.getAddress(), owner, ); const slashingManager = SlashingManagerFactory.connect( - await slashingManagerContract.slashingManager.getAddress(), + await _slashingManager.getAddress(), owner, ); const ciphernodeRegistry = CiphernodeRegistryOwnableFactory.connect( - await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + await mockCiphernodeRegistry.getAddress(), owner, ); + // ── Wire Up Contracts ────────────────────────────────────────────────────── await ticketToken.setRegistry(await bondingRegistry.getAddress()); await slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), @@ -158,29 +145,21 @@ describe("BondingRegistry", function () { await slashingManager.getAddress(), ); - await usdcToken.mint(ownerAddress, ethers.parseUnits("100000", 6)); - await usdcToken.mint(operator1Address, ethers.parseUnits("100000", 6)); - await usdcToken.mint(operator2Address, ethers.parseUnits("100000", 6)); - await licenseToken.mintAllocation( - ownerAddress, - ethers.parseEther("100000"), - "Test allocation", - ); - await licenseToken.mintAllocation( - operator1Address, - ethers.parseEther("100000"), - "Test allocation", - ); - await licenseToken.mintAllocation( - operator2Address, - ethers.parseEther("100000"), - "Test allocation", - ); + // ── Mint Tokens ──────────────────────────────────────────────────────────── + const USDC_AMOUNT = ethers.parseUnits("100000", 6); + const LICENSE_AMOUNT = ethers.parseEther("100000"); + for (const address of [ownerAddress, operator1Address, operator2Address]) { + await usdcToken.mint(address, USDC_AMOUNT); + await licenseToken.mintAllocation( + address, + LICENSE_AMOUNT, + "Test allocation", + ); + } await licenseToken.setTransferRestriction(false); - const tree = new LeanIMT(hash); - + // ── Return ───────────────────────────────────────────────────────────────── return { bondingRegistry, ticketToken, @@ -188,7 +167,6 @@ describe("BondingRegistry", function () { usdcToken, slashingManager, ciphernodeRegistry, - tree, owner, operator1, operator2, @@ -198,6 +176,7 @@ describe("BondingRegistry", function () { operator1Address, operator2Address, treasuryAddress, + tree: new LeanIMT(hash), }; } diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 570395f84f..9d6fec9fbe 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -81,12 +81,13 @@ describe("CiphernodeRegistryOwnable", function () { await registry.addCiphernode(operatorAddress); } - async function setup() { + // ── Signers ──────────────────────────────────────────────────────────────── const [owner, notTheOwner, operator1, operator2] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); + // ── E3 Program Params ────────────────────────────────────────────────────── const abiCoder = ethers.AbiCoder.defaultAbiCoder(); const polynomial_degree = ethers.toBigInt(2048); const plaintext_modulus = ethers.toBigInt(1032193); @@ -98,28 +99,27 @@ describe("CiphernodeRegistryOwnable", function () { const encryptionSchemeId = "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 1000000, - }, + // ── Token Contracts ──────────────────────────────────────────────────────── + const { mockUSDC: usdcToken } = await ignition.deploy( + MockStableTokenModule, + { + parameters: { MockUSDC: { initialSupply: 1_000_000 } }, }, - }); + ); - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, + const { enclaveToken: licenseToken } = await ignition.deploy( + EnclaveTokenModule, + { + parameters: { EnclaveToken: { owner: ownerAddress } }, }, - }); + ); - const ticketTokenContract = await ignition.deploy( + const { enclaveTicketToken: ticketToken } = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { EnclaveTicketToken: { - baseToken: await usdcContract.mockUSDC.getAddress(), + baseToken: await usdcToken.getAddress(), registry: AddressOne, owner: ownerAddress, }, @@ -127,30 +127,38 @@ describe("CiphernodeRegistryOwnable", function () { }, ); - const slashingManagerContract = await ignition.deploy( - SlashingManagerModule, + // ── Registry & Slashing ──────────────────────────────────────────────────── + const { slashingManager } = await ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin: ownerAddress, + }, + }, + }); + + const { cipherNodeRegistry } = await ignition.deploy( + CiphernodeRegistryModule, { parameters: { - SlashingManager: { - admin: ownerAddress, - bondingRegistry: AddressOne, - ciphernodeRegistry: AddressOne, - enclave: AddressOne, + CiphernodeRegistry: { + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, }, }, }, ); + const registryAddress = await cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); - const bondingRegistryContract = await ignition.deploy( + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), - licenseToken: await enclTokenContract.enclaveToken.getAddress(), - registry: AddressOne, + ticketToken: await ticketToken.getAddress(), + licenseToken: await licenseToken.getAddress(), + registry: registryAddress, slashedFundsTreasury: ownerAddress, ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), @@ -160,18 +168,22 @@ describe("CiphernodeRegistryOwnable", function () { }, }, ); + const bondingRegistry = BondingRegistryFactory.connect( + await _bondingRegistry.getAddress(), + owner, + ); - const enclaveContract = await ignition.deploy(EnclaveModule, { + // ── Enclave ──────────────────────────────────────────────────────────────── + const { enclave: _enclave } = await ignition.deploy(EnclaveModule, { parameters: { Enclave: { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: 60 * 60 * 24 * 30, // 30 days - registry: AddressOne, // placeholder, will be updated - bondingRegistry: - await bondingRegistryContract.bondingRegistry.getAddress(), - e3RefundManager: AddressOne, // placeholder, will be updated - feeToken: await usdcContract.mockUSDC.getAddress(), + registry: registryAddress, + bondingRegistry: await bondingRegistry.getAddress(), + e3RefundManager: AddressOne, // placeholder — updated below + feeToken: await usdcToken.getAddress(), timeoutConfig: { committeeFormationWindow: 3600, dkgWindow: 3600, @@ -181,109 +193,66 @@ describe("CiphernodeRegistryOwnable", function () { }, }, }); - - const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclaveAddress = await _enclave.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - const e3RefundManagerContract = await ignition.deploy( - E3RefundManagerModule, - { - parameters: { - E3RefundManager: { - owner: ownerAddress, - enclave: enclaveAddress, - treasury: ownerAddress, - }, - }, - }, - ); - - const e3RefundManagerAddress = - await e3RefundManagerContract.e3RefundManager.getAddress(); - - await enclave.setE3RefundManager(e3RefundManagerAddress); - - // Deploy CiphernodeRegistry with real Enclave address - const registryContract = await ignition.deploy(CiphernodeRegistryModule, { + const { e3RefundManager } = await ignition.deploy(E3RefundManagerModule, { parameters: { - CiphernodeRegistry: { - enclaveAddress: enclaveAddress, + E3RefundManager: { owner: ownerAddress, - submissionWindow: SORTITION_SUBMISSION_WINDOW, + enclave: enclaveAddress, + treasury: ownerAddress, }, }, }); + await enclave.setE3RefundManager(await e3RefundManager.getAddress()); - const registryAddress = - await registryContract.cipherNodeRegistry.getAddress(); - - const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); - - // Update Enclave with correct registry address - await enclave.setCiphernodeRegistry(registryAddress); - - const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), - owner, - ); - - await ticketTokenContract.enclaveTicketToken.setRegistry( - await bondingRegistry.getAddress(), + // ── Wire Up Contracts ────────────────────────────────────────────────────── + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), ); - await bondingRegistry.setRegistry(registryAddress); + await bondingRegistry.setRewardDistributor(enclaveAddress); await bondingRegistry.setSlashingManager( - await slashingManagerContract.slashingManager.getAddress(), + await slashingManager.getAddress(), ); - await slashingManagerContract.slashingManager.setBondingRegistry( + await slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), ); + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setEnclave(enclaveAddress); - // Set up mock E3Program and DecryptionVerifier for Enclave - const mockE3Program = await ignition.deploy(MockE3ProgramModule); - const mockDecryptionVerifier = await ignition.deploy( + // ── Mock E3 Program & Decryption Verifier ────────────────────────────────── + const { mockE3Program } = await ignition.deploy(MockE3ProgramModule); + const { mockDecryptionVerifier } = await ignition.deploy( MockDecryptionVerifierModule, ); - await enclave.enableE3Program( - await mockE3Program.mockE3Program.getAddress(), - ); + await enclave.enableE3Program(await mockE3Program.getAddress()); await enclave.setE3ProgramsParams([encodedE3ProgramParams]); await enclave.setDecryptionVerifier( encryptionSchemeId, - await mockDecryptionVerifier.mockDecryptionVerifier.getAddress(), + await mockDecryptionVerifier.getAddress(), ); - await bondingRegistry.setRewardDistributor(enclaveAddress); - - await registry.setBondingRegistry(await bondingRegistry.getAddress()); - - const tree = new LeanIMT(hash); - const licenseToken = enclTokenContract.enclaveToken; - const ticketToken = ticketTokenContract.enclaveTicketToken; - const usdcToken = usdcContract.mockUSDC; - + // ── Operators ────────────────────────────────────────────────────────────── await licenseToken.setTransferRestriction(false); - await setupOperatorForSortition( - operator1, - bondingRegistry, - licenseToken, - usdcToken, - ticketToken, - registry, - ); - tree.insert(BigInt(await operator1.getAddress())); + const tree = new LeanIMT(hash); - await setupOperatorForSortition( - operator2, - bondingRegistry, - licenseToken, - usdcToken, - ticketToken, - registry, - ); - tree.insert(BigInt(await operator2.getAddress())); + for (const operator of [operator1, operator2]) { + await setupOperatorForSortition( + operator, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + registry, + ); + tree.insert(BigInt(await operator.getAddress())); + } await networkHelpers.mine(1); + // ── Return ───────────────────────────────────────────────────────────────── return { owner, notTheOwner, @@ -326,11 +295,11 @@ describe("CiphernodeRegistryOwnable", function () { const requestParams = { threshold: [2, 2] as [number, number], inputWindow: [currentTime + 100, currentTime + 300] as [number, number], - e3Program: await mockE3Program.mockE3Program.getAddress(), + e3Program: await mockE3Program.getAddress(), e3ProgramParams: encodedE3ProgramParams, computeProviderParams: abiCoder.encode( ["address"], - [await mockDecryptionVerifier.mockDecryptionVerifier.getAddress()], + [await mockDecryptionVerifier.getAddress()], ), customParams: abiCoder.encode( ["address"], @@ -369,7 +338,7 @@ describe("CiphernodeRegistryOwnable", function () { const initData = ciphernodeRegistryFactory.interface.encodeFunctionData( "initialize", - [deployer.address, AddressTwo, SORTITION_SUBMISSION_WINDOW], + [deployer.address, SORTITION_SUBMISSION_WINDOW], ); const proxyFactory = await ethers.getContractFactory( @@ -389,7 +358,6 @@ describe("CiphernodeRegistryOwnable", function () { ); expect(await ciphernodeRegistry.owner()).to.equal(deployer.address); - expect(await ciphernodeRegistry.enclave()).to.equal(AddressTwo); expect(await ciphernodeRegistry.sortitionSubmissionWindow()).to.equal( SORTITION_SUBMISSION_WINDOW, ); diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts index 2e04cc9a7e..cb1ee917df 100644 --- a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -121,8 +121,8 @@ describe("Committee Expulsion & Fault Tolerance", function () { [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], ); } - const setup = async () => { + // ── Signers ──────────────────────────────────────────────────────────────── const [ owner, requester, @@ -137,25 +137,25 @@ describe("Committee Expulsion & Fault Tolerance", function () { const treasuryAddress = await treasury.getAddress(); const requesterAddress = await requester.getAddress(); - // Deploy tokens - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { MockUSDC: { initialSupply: 10000000 } }, + // ── Tokens ───────────────────────────────────────────────────────────────── + const { mockUSDC } = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 10_000_000 } }, }); const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), + await mockUSDC.getAddress(), owner, ); - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + const { enclaveToken } = await ignition.deploy(EnclaveTokenModule, { parameters: { EnclaveToken: { owner: ownerAddress } }, }); const enclToken = EnclaveTokenFactory.connect( - await enclTokenContract.enclaveToken.getAddress(), + await enclaveToken.getAddress(), owner, ); await enclToken.setTransferRestriction(false); - const ticketTokenContract = await ignition.deploy( + const { enclaveTicketToken } = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { @@ -167,45 +167,58 @@ describe("Committee Expulsion & Fault Tolerance", function () { }, }, ); + const ticketToken = enclaveTicketToken; - const mockVerifierContract = await ignition.deploy( + const { mockCircuitVerifier } = await ignition.deploy( MockCircuitVerifierModule, ); const mockVerifier = MockCircuitVerifierFactory.connect( - await mockVerifierContract.mockCircuitVerifier.getAddress(), + await mockCircuitVerifier.getAddress(), owner, ); - // Deploy slashing manager - const slashingManagerContract = await ignition.deploy( + // ── Registry & Slashing ──────────────────────────────────────────────────── + const { slashingManager: _slashingManager } = await ignition.deploy( SlashingManagerModule, { parameters: { SlashingManager: { admin: ownerAddress, - bondingRegistry: addressOne, - ciphernodeRegistry: addressOne, - enclave: addressOne, }, }, }, ); const slashingManager = SlashingManagerFactory.connect( - await slashingManagerContract.slashingManager.getAddress(), + await _slashingManager.getAddress(), owner, ); - // Deploy bonding registry - const bondingRegistryContract = await ignition.deploy( + const { cipherNodeRegistry } = await ignition.deploy( + CiphernodeRegistryModule, + { + parameters: { + CiphernodeRegistry: { + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }, + ); + const registryAddress = await cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryOwnableFactory.connect( + registryAddress, + owner, + ); + + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), + ticketToken: await ticketToken.getAddress(), licenseToken: await enclToken.getAddress(), - registry: addressOne, + registry: registryAddress, slashedFundsTreasury: treasuryAddress, ticketPrice: ethers.parseUnits("10", 6), licenseRequiredBond: ethers.parseEther("1000"), @@ -216,18 +229,18 @@ describe("Committee Expulsion & Fault Tolerance", function () { }, ); const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), + await _bondingRegistry.getAddress(), owner, ); - // Deploy Enclave - const enclaveContract = await ignition.deploy(EnclaveModule, { + // ── Enclave ──────────────────────────────────────────────────────────────── + const { enclave: _enclave } = await ignition.deploy(EnclaveModule, { parameters: { Enclave: { params: encodedE3ProgramParams, owner: ownerAddress, maxDuration: THIRTY_DAYS, - registry: addressOne, + registry: registryAddress, e3RefundManager: addressOne, bondingRegistry: await bondingRegistry.getAddress(), feeToken: await usdcToken.getAddress(), @@ -235,53 +248,31 @@ describe("Committee Expulsion & Fault Tolerance", function () { }, }, }); - const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclaveAddress = await _enclave.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); - // Deploy CiphernodeRegistry - const ciphernodeRegistryContract = await ignition.deploy( - CiphernodeRegistryModule, - { - parameters: { - CiphernodeRegistry: { - enclaveAddress: enclaveAddress, - owner: ownerAddress, - submissionWindow: SORTITION_SUBMISSION_WINDOW, - }, - }, - }, - ); - const registryAddress = - await ciphernodeRegistryContract.cipherNodeRegistry.getAddress(); - const registry = CiphernodeRegistryOwnableFactory.connect( - registryAddress, - owner, - ); - - // Deploy mock E3 program - const e3ProgramContract = await ignition.deploy(MockE3ProgramModule, { - parameters: { - MockE3Program: { - encryptionSchemeId: encryptionSchemeId, - }, - }, + // ── Mocks ────────────────────────────────────────────────────────────────── + const { mockE3Program } = await ignition.deploy(MockE3ProgramModule, { + parameters: { MockE3Program: { encryptionSchemeId } }, }); const e3Program = MockE3ProgramFactory.connect( - await e3ProgramContract.mockE3Program.getAddress(), + await mockE3Program.getAddress(), owner, ); - // Deploy mock decryption verifier - const decryptionVerifierContract = await ignition.deploy( + const { mockDecryptionVerifier } = await ignition.deploy( MockDecryptionVerifierModule, ); const decryptionVerifier = MockDecryptionVerifierFactory.connect( - await decryptionVerifierContract.mockDecryptionVerifier.getAddress(), + await mockDecryptionVerifier.getAddress(), owner, ); - // Wire everything together - await enclave.setCiphernodeRegistry(registryAddress); + // ── Wire Up ──────────────────────────────────────────────────────────────── + await registry.setEnclave(enclaveAddress); + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setSlashingManager(await slashingManager.getAddress()); + await enclave.enableE3Program(await e3Program.getAddress()); await enclave.setDecryptionVerifier( encryptionSchemeId, @@ -290,7 +281,6 @@ describe("Committee Expulsion & Fault Tolerance", function () { await enclave.setSlashingManager(await slashingManager.getAddress()); await bondingRegistry.setRewardDistributor(enclaveAddress); - await bondingRegistry.setRegistry(registryAddress); await bondingRegistry.setSlashingManager( await slashingManager.getAddress(), ); @@ -301,19 +291,34 @@ describe("Committee Expulsion & Fault Tolerance", function () { await slashingManager.setCiphernodeRegistry(registryAddress); await slashingManager.setEnclave(enclaveAddress); - await registry.setBondingRegistry(await bondingRegistry.getAddress()); - await registry.setSlashingManager(await slashingManager.getAddress()); + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await usdcToken.mint(requesterAddress, ethers.parseUnits("100000", 6)); - await ticketTokenContract.enclaveTicketToken.setRegistry( - await bondingRegistry.getAddress(), - ); + // ── Slash Policies ───────────────────────────────────────────────────────── + const baseSlashPolicy = { + ticketPenalty: ethers.parseUnits("10", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + }; - // Mint tokens to requester for E3 requests - await usdcToken.mint(requesterAddress, ethers.parseUnits("100000", 6)); + await slashingManager.setSlashPolicy(REASON_BAD_DKG, { + ...baseSlashPolicy, + failureReason: 4, // FailureReason.DKGInvalidShares + }); + await slashingManager.setSlashPolicy(REASON_BAD_DECRYPTION, { + ...baseSlashPolicy, + failureReason: 11, // FailureReason.DecryptionInvalidShares + }); - // Helper: setup an operator (bond license, register, add tickets) + // ── Helpers ──────────────────────────────────────────────────────────────── async function setupOperator(operator: Signer) { const operatorAddress = await operator.getAddress(); + await enclToken.mintAllocation( operatorAddress, ethers.parseEther("10000"), @@ -329,19 +334,17 @@ describe("Committee Expulsion & Fault Tolerance", function () { .bondLicense(ethers.parseEther("1000")); await bondingRegistry.connect(operator).registerOperator(); - const ticketTokenAddress = await bondingRegistry.ticketToken(); const ticketAmount = ethers.parseUnits("100", 6); await usdcToken .connect(operator) - .approve(ticketTokenAddress, ticketAmount); + .approve(await bondingRegistry.ticketToken(), ticketAmount); await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); } - // Helper: make an E3 request async function makeRequest(threshold: [number, number] = [2, 3]) { const startTime = (await time.latest()) + 100; const requestParams = { - threshold: threshold, + threshold, inputWindow: [startTime + 100, startTime + ONE_DAY] as [number, number], e3Program: await e3Program.getAddress(), e3ProgramParams: encodedE3ProgramParams, @@ -360,55 +363,23 @@ describe("Committee Expulsion & Fault Tolerance", function () { await enclave.connect(requester).request(requestParams); } - // Helper: finalize a committee after sortition async function finalizeCommitteeWithOperators( e3Id: number, operators: Signer[], ) { - for (const op of operators) { + for (const op of operators) await registry.connect(op).submitTicket(e3Id, 1); - } + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); await registry.finalizeCommittee(e3Id); - // Publish the committee key so getCommitteeNodes works const nodes = await Promise.all(operators.map((op) => op.getAddress())); const publicKey = ethers.toUtf8Bytes("fake-public-key"); const publicKeyHash = ethers.keccak256(publicKey); await registry.publishCommittee(e3Id, nodes, publicKey, publicKeyHash); } - // Set up committee-affecting slash policy - // MockCircuitVerifier returns false by default → proof invalid → fault confirmed - const committeeSlashPolicy = { - ticketPenalty: ethers.parseUnits("10", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), - banNode: false, - appealWindow: 0, - enabled: true, - affectsCommittee: true, - failureReason: 4, // FailureReason.DKGInvalidShares - }; - await slashingManager.setSlashPolicy(REASON_BAD_DKG, committeeSlashPolicy); - - const decryptionSlashPolicy = { - ticketPenalty: ethers.parseUnits("10", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), - banNode: false, - appealWindow: 0, - enabled: true, - affectsCommittee: true, - failureReason: 11, // FailureReason.DecryptionInvalidShares - }; - await slashingManager.setSlashPolicy( - REASON_BAD_DECRYPTION, - decryptionSlashPolicy, - ); - + // ── Return ───────────────────────────────────────────────────────────────── return { enclave, registry, @@ -417,6 +388,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { mockVerifier, usdcToken, enclToken, + ticketToken, owner, requester, treasury, diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index f96574724a..f9751a8c43 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -154,33 +154,28 @@ describe("SlashingManager", function () { } async function setup() { + // ── Signers ──────────────────────────────────────────────────────────────── const [owner, slasher, proposer, operator, notTheOwner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); const operatorAddress = await operator.getAddress(); - const usdcContract = await ignition.deploy(MockStableTokenModule, { - parameters: { - MockUSDC: { - initialSupply: 1000000, - }, - }, + // ── Token Contracts ──────────────────────────────────────────────────────── + const { mockUSDC } = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 1_000_000 } }, }); - - const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { - parameters: { - EnclaveToken: { - owner: ownerAddress, - }, + const { enclaveToken: _enclaveToken } = await ignition.deploy( + EnclaveTokenModule, + { + parameters: { EnclaveToken: { owner: ownerAddress } }, }, - }); - - const ticketTokenContract = await ignition.deploy( + ); + const { enclaveTicketToken } = await ignition.deploy( EnclaveTicketTokenModule, { parameters: { EnclaveTicketToken: { - baseToken: await usdcContract.mockUSDC.getAddress(), + baseToken: await mockUSDC.getAddress(), registry: ownerAddress, owner: ownerAddress, }, @@ -188,40 +183,35 @@ describe("SlashingManager", function () { }, ); - const mockVerifierContract = await ignition.deploy( + // ── Mock Contracts ───────────────────────────────────────────────────────── + const { mockCircuitVerifier } = await ignition.deploy( MockCircuitVerifierModule, ); - - const mockCiphernodeRegistryContract = await ignition.deploy( - MockCiphernodeRegistryModule, - ); - + const { mockCiphernodeRegistry: _mockCiphernodeRegistry } = + await ignition.deploy(MockCiphernodeRegistryModule); const mockCiphernodeRegistryAddress = - await mockCiphernodeRegistryContract.mockCiphernodeRegistry.getAddress(); + await _mockCiphernodeRegistry.getAddress(); - const slashingManagerContract = await ignition.deploy( + // ── Slashing & Bonding ───────────────────────────────────────────────────── + const { slashingManager: _slashingManager } = await ignition.deploy( SlashingManagerModule, { parameters: { SlashingManager: { admin: ownerAddress, - bondingRegistry: ownerAddress, - ciphernodeRegistry: mockCiphernodeRegistryAddress, - enclave: addressOne, }, }, }, ); - const bondingRegistryContract = await ignition.deploy( + const { bondingRegistry: _bondingRegistry } = await ignition.deploy( BondingRegistryModule, { parameters: { BondingRegistry: { owner: ownerAddress, - ticketToken: - await ticketTokenContract.enclaveTicketToken.getAddress(), - licenseToken: await enclTokenContract.enclaveToken.getAddress(), + ticketToken: await enclaveTicketToken.getAddress(), + licenseToken: await _enclaveToken.getAddress(), registry: ethers.ZeroAddress, slashedFundsTreasury: ownerAddress, ticketPrice: ethers.parseUnits("10", 6), @@ -233,20 +223,21 @@ describe("SlashingManager", function () { }, ); + // ── Connect Factories ────────────────────────────────────────────────────── const usdcToken = MockUSDCFactory.connect( - await usdcContract.mockUSDC.getAddress(), + await mockUSDC.getAddress(), owner, ); const enclaveToken = EnclaveTokenFactory.connect( - await enclTokenContract.enclaveToken.getAddress(), + await _enclaveToken.getAddress(), owner, ); const ticketToken = EnclaveTicketTokenFactory.connect( - await ticketTokenContract.enclaveTicketToken.getAddress(), + await enclaveTicketToken.getAddress(), owner, ); const mockVerifier = MockCircuitVerifierFactory.connect( - await mockVerifierContract.mockCircuitVerifier.getAddress(), + await mockCircuitVerifier.getAddress(), owner, ); const mockCiphernodeRegistry = MockCiphernodeRegistryFactory.connect( @@ -254,14 +245,15 @@ describe("SlashingManager", function () { owner, ); const slashingManager = SlashingManagerFactory.connect( - await slashingManagerContract.slashingManager.getAddress(), + await _slashingManager.getAddress(), owner, ); const bondingRegistry = BondingRegistryFactory.connect( - await bondingRegistryContract.bondingRegistry.getAddress(), + await _bondingRegistry.getAddress(), owner, ); + // ── Wire Up & Configure ──────────────────────────────────────────────────── await ticketToken.setRegistry(await bondingRegistry.getAddress()); await slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), @@ -271,15 +263,14 @@ describe("SlashingManager", function () { ); await enclaveToken.setTransferRestriction(false); - await enclaveToken.mintAllocation( operatorAddress, ethers.parseEther("2000"), "Test allocation", ); - await slashingManager.addSlasher(await slasher.getAddress()); + // ── Return ───────────────────────────────────────────────────────────────── return { owner, slasher, From ab4f48ce505e840355b08393f3f176fdbfe021bd Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 24 Feb 2026 02:08:04 +0500 Subject: [PATCH 11/21] fix: fix tests --- packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index f9751a8c43..8c2db3a6b2 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -269,6 +269,7 @@ describe("SlashingManager", function () { "Test allocation", ); await slashingManager.addSlasher(await slasher.getAddress()); + await slashingManager.setCiphernodeRegistry(mockCiphernodeRegistryAddress); // ── Return ───────────────────────────────────────────────────────────────── return { From 5b30e09cb8f025533fd5fadf683ff292ec485b38 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 24 Feb 2026 15:28:31 +0500 Subject: [PATCH 12/21] fix: contract addresses --- examples/CRISP/enclave.config.yaml | 4 +- examples/CRISP/server/.env.example | 2 +- .../contracts/slashing/SlashingManager.sol | 45 +++++-------------- templates/default/enclave.config.yaml | 8 ++-- 4 files changed, 18 insertions(+), 41 deletions(-) diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index be50309cb8..f2965aff5e 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -9,10 +9,10 @@ chains: address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' deploy_block: 13 ciphernode_registry: - address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' + address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' deploy_block: 11 bonding_registry: - address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' + address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' deploy_block: 8 fee_token: address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 79e0b1c066..7d27afc2b3 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -14,7 +14,7 @@ CRON_API_KEY=1234567890 # Based on Default Hardhat Deployments (Only for testing) ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" -CIPHERNODE_REGISTRY_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" +CIPHERNODE_REGISTRY_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" E3_PROGRAM_ADDRESS="0x851356ae760d987E095750cCeb3bC6014560891C" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index f392d6f4ca..9454b29629 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -213,18 +213,9 @@ contract SlashingManager is ISlashingManager, AccessControl { /// uint256 chainId, /// uint256 proofType, /// address verifier)` - /// The operator must have signed: - /// `keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, - /// chainId, - /// e3Id, - /// proofType, - /// keccak256(zkProof), - /// keccak256(abi.encodePacked(publicInputs))))` - /// This prevents: - /// - Arbitrary proof submission (attacker can't forge operator's signature) - /// - Cross-E3 replay (e3Id is in the signed message)` - /// - Cross-chain replay (chainId is in the signed message)` - /// - Verifier-upgrade attacks (verifier in evidence must match policy's current verifier)` + /// Operator must sign the EIP-191 prefixed payload hash via `personal_sign`/`signMessage()` + /// (NOT raw `eth_signHash`): `personal_sign(keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, + /// chainId, e3Id, proofType, keccak256(zkProof), keccak256(abi.encodePacked(publicInputs)))))` function proposeSlash( uint256 e3Id, address operator, @@ -358,14 +349,9 @@ contract SlashingManager is ISlashingManager, AccessControl { // Internal Execution // ====================== - /// @dev Decodes evidence, verifies operator signature, committee membership, - /// and that the ZK proof is invalid (fault confirmed). - /// Evidence format: - /// `abi.encode(bytes zkProof, bytes32[] publicInputs, - /// bytes signature, - /// uint256 chainId, - /// uint256 proofType, - /// address verifier)` + /// @dev Decodes and verifies: verifier match, chainId, operator EIP-191 signature, committee + /// membership, and that the ZK proof is invalid (fault confirmed). Same evidence format as + /// `proposeSlash` — see its `@dev` for the `abi.encode` layout. function _verifyProofEvidence( bytes calldata proof, uint256 e3Id, @@ -425,16 +411,9 @@ contract SlashingManager is ISlashingManager, AccessControl { require(!proofValid, ProofIsValid()); } - /** - * @notice Internal function that executes a slash and handles committee expulsion - * @dev For Lane B (delayed execution), the operator may have deregistered during the appeal - * window. BondingRegistry.slashTicketBalance and slashLicenseBond use Math.min(requested, - * available), so zero-balance operators receive a zero slash gracefully. The exit queue's - * slashPendingAssets(includeLockedAssets=true) covers operators mid-exit. If the operator - * has already claimed their exit, funds are gone and the slash amount becomes 0. This is - * an accepted tradeoff for the appeal window design. - * @param proposalId ID of the proposal to execute - */ + /// @dev Executes a slash: applies financial penalties, optional ban, and committee expulsion. + /// Lane B: if the operator deregistered or exited during the appeal window, penalties + /// gracefully become 0 (BondingRegistry uses min(requested, available)). Accepted tradeoff. function _executeSlash(uint256 proposalId) internal { SlashProposal storage p = _proposals[proposalId]; p.executed = true; @@ -491,10 +470,8 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @inheritdoc ISlashingManager - /// @dev Only the accused operator can file an appeal. No delegate, multi-sig, or representative - /// patterns exist. If the operator has lost access to their key or been banned, they cannot - /// appeal. Consider adding an appealDelegate mapping for production to allow a designated - /// representative to appeal on behalf of the operator. + /// @dev Only the accused operator may appeal (no delegate support). Consider an `appealDelegate` + /// mapping for production to handle lost-key or banned-operator scenarios. function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index dd0161545a..661c745f2e 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -9,14 +9,14 @@ chains: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" deploy_block: 1 # Set to actual deploy block ciphernode_registry: - address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block bonding_registry: - address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" deploy_block: 1 # Set to actual deploy block slashing_manager: - address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' - deploy_block: 8 + address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" + deploy_block: 1 # Set to actual deploy block fee_token: address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" deploy_block: 1 # Set to actual deploy block From 256a3c06652e76548ac8595271d47c1e088239e7 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 24 Feb 2026 16:13:46 +0500 Subject: [PATCH 13/21] fix: dev note natspec comment --- .../enclave-contracts/contracts/slashing/SlashingManager.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 9454b29629..94b34798e8 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -350,8 +350,8 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @dev Decodes and verifies: verifier match, chainId, operator EIP-191 signature, committee - /// membership, and that the ZK proof is invalid (fault confirmed). Same evidence format as - /// `proposeSlash` — see its `@dev` for the `abi.encode` layout. + /// membership, and that the ZK proof is invalid (fault confirmed). Evidence encoding + /// matches proposeSlash — see that function's dev note for the abi.encode layout. function _verifyProofEvidence( bytes calldata proof, uint256 e3Id, From 34ed209469c80f59e62de27b5aa66b43f4f288b9 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Tue, 24 Feb 2026 22:59:17 +0500 Subject: [PATCH 14/21] feat: route slashed funds --- .../IBondingRegistry.json | 28 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 39 ++- .../ISlashingManager.json | 73 +++- .../EnclaveTicketToken.json | 2 +- .../DkgPkVerifier.sol/DkgPkVerifier.json | 8 +- .../DkgPkVerifier.sol/ZKTranscriptLib.json | 2 +- .../contracts/E3RefundManager.sol | 45 ++- .../enclave-contracts/contracts/Enclave.sol | 15 + .../contracts/interfaces/IBondingRegistry.sol | 11 +- .../contracts/interfaces/IE3RefundManager.sol | 7 + .../contracts/interfaces/IEnclave.sol | 11 + .../contracts/interfaces/ISlashingManager.sol | 38 +- .../contracts/registry/BondingRegistry.sol | 19 +- .../contracts/slashing/SlashingManager.sol | 85 ++++- .../scripts/deployEnclave.ts | 3 + .../test/E3Lifecycle/E3Integration.spec.ts | 328 +++++++++++++++++- .../test/Slashing/CommitteeExpulsion.spec.ts | 1 + .../test/Slashing/SlashingManager.spec.ts | 2 + 19 files changed, 672 insertions(+), 47 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index dca24c8413..5b76ab1824 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -610,6 +610,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "redirectSlashedTicketFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "registerOperator", @@ -828,7 +846,13 @@ } ], "name": "slashTicketBalance", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "actualAmount", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -922,5 +946,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 12c2ba2ced..3a91789b7f 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -787,5 +787,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index fe3403cb14..47a26f89ee 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -433,6 +433,25 @@ "name": "RewardsDistributed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SlashedFundsRouted", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1106,6 +1125,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "routeSlashedFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1226,5 +1263,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json index 3ca8cb1cf7..71e4ec2343 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -222,6 +222,25 @@ "name": "NodeBanUpdated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RoutingFailed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -392,6 +411,25 @@ "name": "SlashProposed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SlashedFundsRoutedToRefund", + "type": "event" + }, { "inputs": [ { @@ -747,7 +785,25 @@ { "inputs": [ { - "internalType": "address", + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "routeSlashedFundsToRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IBondingRegistry", "name": "newBondingRegistry", "type": "address" } @@ -757,6 +813,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract IE3RefundManager", + "name": "newRefundManager", + "type": "address" + } + ], + "name": "setE3RefundManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -865,5 +934,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", - "buildInfoId": "solc-0_8_28-70641a62ef471f613aadde1ccfd9fba9a3e44fb0" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index 742068c990..09f0432af8 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -1223,5 +1223,5 @@ ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json index 7b356ea230..1035a2e869 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "33966": [ + "34324": [ { "length": 32, "start": 91 @@ -164,13 +164,13 @@ "start": 11964 } ], - "33968": [ + "34326": [ { "length": 32, "start": 398 } ], - "33970": [ + "34328": [ { "length": 32, "start": 432 @@ -182,5 +182,5 @@ ] }, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json index d13bc6ac01..c975d73910 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json @@ -396,5 +396,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-15d7734d5573c9c7fd167defb7acf0fe3bbd3190" + "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index aa4d8def71..8d0683e475 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -52,6 +52,8 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses mapping(uint256 e3Id => address[] nodes) internal _honestNodes; + /// @notice Tracks pending slashed funds for E3s whose refund hasn't been calculated yet + mapping(uint256 e3Id => uint256 amount) internal _pendingSlashedFunds; //////////////////////////////////////////////////////////// // // // Modifiers // @@ -139,7 +141,8 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { totalSlashed: 0, honestNodeCount: honestNodes.length, calculated: true, - feeToken: paymentToken + feeToken: paymentToken, + originalPayment: originalPayment }); // Store honest nodes @@ -152,6 +155,13 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { paymentToken.safeTransfer(treasury, protocolAmount); } + // Apply any slashed funds that arrived before the distribution was calculated + uint256 pending = _pendingSlashedFunds[e3Id]; + if (pending > 0) { + _pendingSlashedFunds[e3Id] = 0; + _applySlashedFunds(e3Id, pending); + } + emit RefundDistributionCalculated( e3Id, requesterAmount, @@ -319,14 +329,36 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { uint256 e3Id, uint256 amount ) external onlyEnclave { + require(amount > 0, "Zero amount"); + RefundDistribution storage dist = _distributions[e3Id]; - require(dist.calculated, "Not calculated"); + if (!dist.calculated) { + // Distribution not yet calculated; queue for later application + // when processE3Failure -> calculateRefund is called. + _pendingSlashedFunds[e3Id] += amount; + emit SlashedFundsRouted(e3Id, amount); + return; + } + require(_claimCount[e3Id] == 0, "Claims already started"); - require(amount > 0, "Zero amount"); + _applySlashedFunds(e3Id, amount); + } + + /// @notice Apply slashed funds to an E3's refund distribution + /// @dev Priority: make requester whole first, then distribute remainder to honest nodes. + /// The requester is filled up to their original E3 payment before honest nodes receive + /// any portion, ensuring the party who paid for the computation is compensated first. + /// @param e3Id The E3 ID + /// @param amount The slashed amount to apply + function _applySlashedFunds(uint256 e3Id, uint256 amount) internal { + RefundDistribution storage dist = _distributions[e3Id]; - // Add slashed funds to distribution - // 50% to requester, 50% to honest nodes for non-participation - uint256 toRequester = amount / 2; + // Priority: make requester whole first + // requesterGap = how much more the requester needs to reach their original payment + uint256 requesterGap = dist.originalPayment > dist.requesterAmount + ? dist.originalPayment - dist.requesterAmount + : 0; + uint256 toRequester = amount >= requesterGap ? requesterGap : amount; uint256 toHonestNodes = amount - toRequester; dist.requesterAmount += toRequester; @@ -334,6 +366,7 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { dist.totalSlashed += amount; emit SlashedFundsRouted(e3Id, amount); + emit SlashedFundsApplied(e3Id, toRequester, toHonestNodes); } //////////////////////////////////////////////////////////// diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 75d7af278a..0d58672db6 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -225,6 +225,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { _; } + /// @notice Restricts function to SlashingManager contract only + modifier onlySlashingManager() { + require(msg.sender == address(slashingManager), "Only SlashingManager"); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -728,6 +734,15 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3FailureProcessed(e3Id, payment, honestNodes.length); } + /// @inheritdoc IEnclave + function routeSlashedFunds( + uint256 e3Id, + uint256 amount + ) external onlySlashingManager { + e3RefundManager.routeSlashedFunds(e3Id, amount); + emit SlashedFundsRouted(e3Id, amount); + } + /// @inheritdoc IEnclave function onCommitteeFinalized( uint256 e3Id diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 61b6fa0fb8..83a43d8389 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -337,7 +337,7 @@ interface IBondingRegistry { address operator, uint256 amount, bytes32 reason - ) external; + ) external returns (uint256 actualAmount); /** * @notice Slash operator's license bond by absolute amount @@ -352,6 +352,15 @@ interface IBondingRegistry { bytes32 reason ) external; + /** + * @notice Redirect slashed ticket funds to a specified address + * @param to Address to receive the slashed funds (underlying stablecoin) + * @param amount Amount of slashed ticket balance to redirect + * @dev Only callable by authorized slashing manager. Pays out underlying stablecoin + * from burned ticket tokens. Assumes underlying stablecoin matches the E3 fee token. + */ + function redirectSlashedTicketFunds(address to, uint256 amount) external; + // ====================== // Reward Distribution Functions // ====================== diff --git a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol index 44a079494b..6583bae279 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol @@ -34,6 +34,7 @@ interface IE3RefundManager { uint256 honestNodeCount; // Number of honest nodes bool calculated; // Whether distribution is calculated IERC20 feeToken; // The fee token used for this E3's payment (stored per-E3 to survive token rotations) + uint256 originalPayment; // Original E3 payment amount (for making requester whole) } //////////////////////////////////////////////////////////// // // @@ -57,6 +58,12 @@ interface IE3RefundManager { ); /// @notice Emitted when slashed funds are routed to E3 event SlashedFundsRouted(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted when slashed funds are applied to refund distribution + event SlashedFundsApplied( + uint256 indexed e3Id, + uint256 toRequester, + uint256 toHonestNodes + ); /// @notice Emitted when work allocation is updated event WorkAllocationUpdated(WorkValueAllocation allocation); //////////////////////////////////////////////////////////// diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index f4d65d5a42..5cdb473a03 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -164,6 +164,11 @@ interface IEnclave { /// @param slashingManager The address of the SlashingManager contract. event SlashingManagerSet(address indexed slashingManager); + /// @notice Emitted when slashed funds are routed to E3 refund pool + /// @param e3Id The E3 ID. + /// @param amount The amount of slashed funds routed. + event SlashedFundsRouted(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted when a failed E3 is processed for refunds. /// @param e3Id The ID of the failed E3. /// @param paymentAmount The original payment amount being refunded. @@ -372,6 +377,12 @@ interface IEnclave { /// @param reason The failure reason from FailureReason enum. function onE3Failed(uint256 e3Id, uint8 reason) external; + /// @notice Routes slashed ticket funds to the E3 refund pool + /// @dev Called by SlashingManager. Proxies to E3RefundManager.routeSlashedFunds. + /// @param e3Id The E3 ID. + /// @param amount Amount of slashed funds to route. + function routeSlashedFunds(uint256 e3Id, uint256 amount) external; + //////////////////////////////////////////////////////////// // // // Lifecycle Functions // diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index 9cf3c4089d..f28c0244bf 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -7,6 +7,7 @@ pragma solidity >=0.8.27; import { IBondingRegistry } from "./IBondingRegistry.sol"; +import { IE3RefundManager } from "./IE3RefundManager.sol"; /** * @title ISlashingManager @@ -259,6 +260,20 @@ interface ISlashingManager { address updater ); + /** + * @notice Emitted when slashed ticket funds are routed to the E3 refund pool + * @param e3Id ID of the E3 computation + * @param amount Amount of slashed funds routed (underlying stablecoin) + */ + event SlashedFundsRoutedToRefund(uint256 indexed e3Id, uint256 amount); + + /** + * @notice Emitted when routing slashed funds fails (funds remain in BondingRegistry) + * @param e3Id ID of the E3 computation + * @param amount Amount that failed to route + */ + event RoutingFailed(uint256 indexed e3Id, uint256 amount); + // ====================== // View Functions // ====================== @@ -326,11 +341,18 @@ interface ISlashingManager { ) external; /** - * @notice Updates the bonding registry contract address + * @notice Updates the bonding registry contract * @dev Only callable by DEFAULT_ADMIN_ROLE. Used to execute actual slashing of funds - * @param newBondingRegistry Address of the new IBondingRegistry contract (must be non-zero) + * @param newBondingRegistry The new IBondingRegistry contract (must be non-zero) */ - function setBondingRegistry(address newBondingRegistry) external; + function setBondingRegistry(IBondingRegistry newBondingRegistry) external; + + /** + * @notice Updates the E3 Refund Manager contract + * @dev Only callable by DEFAULT_ADMIN_ROLE + * @param newRefundManager The new IE3RefundManager contract (must be non-zero) + */ + function setE3RefundManager(IE3RefundManager newRefundManager) external; /** * @notice Grants SLASHER_ROLE to an address @@ -401,6 +423,16 @@ interface ISlashingManager { */ function executeSlash(uint256 proposalId) external; + /** + * @notice Atomically redirects slashed ticket funds and updates E3 refund distribution + * @dev Only callable by this contract (self-call pattern for try/catch atomicity). + * Transfers underlying stablecoin from BondingRegistry to E3RefundManager + * and calls Enclave.routeSlashedFunds to update the distribution. + * @param e3Id ID of the E3 computation + * @param amount Amount of slashed ticket balance to route + */ + function routeSlashedFundsToRefund(uint256 e3Id, uint256 amount) external; + // ====================== // Appeal Functions // ====================== diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index d9fcbb6ad7..924461c823 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -471,7 +471,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { address operator, uint256 requestedSlashAmount, bytes32 slashReason - ) external onlySlashingManager { + ) external onlySlashingManager returns (uint256) { require(requestedSlashAmount != 0, ZeroAmount()); (uint256 pendingTicketBalance, ) = _exits.getPendingAmounts(operator); @@ -484,7 +484,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); if (actualSlashAmount == 0) { - return; + return 0; } // Slash from active balance first @@ -516,6 +516,8 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { ); _updateOperatorStatus(operator); + + return actualSlashAmount; } /// @inheritdoc IBondingRegistry @@ -568,6 +570,19 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _updateOperatorStatus(operator); } + /// @inheritdoc IBondingRegistry + function redirectSlashedTicketFunds( + address to, + uint256 amount + ) external onlySlashingManager { + require(to != address(0), ZeroAddress()); + require(amount > 0, ZeroAmount()); + require(amount <= slashedTicketBalance, InsufficientBalance()); + + slashedTicketBalance -= amount; + ticketToken.payout(to, amount); + } + // ====================== // Reward Distribution Functions // ====================== diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 94b34798e8..77cc3cd791 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -17,6 +17,7 @@ import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IEnclave } from "../interfaces/IEnclave.sol"; +import { IE3RefundManager } from "../interfaces/IE3RefundManager.sol"; import { ICircuitVerifier } from "../interfaces/ICircuitVerifier.sol"; /** @@ -51,6 +52,9 @@ contract SlashingManager is ISlashingManager, AccessControl { /// @notice Reference to the Enclave contract for E3 failure signaling IEnclave public enclave; + /// @notice Reference to the E3 Refund Manager for routing slashed funds + IE3RefundManager public e3RefundManager; + /// @notice Mapping from slash reason hash to its configured policy mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; @@ -163,28 +167,36 @@ contract SlashingManager is ISlashingManager, AccessControl { /// @inheritdoc ISlashingManager function setBondingRegistry( - address newBondingRegistry + IBondingRegistry newBondingRegistry ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(newBondingRegistry != address(0), ZeroAddress()); - bondingRegistry = IBondingRegistry(newBondingRegistry); + require(address(newBondingRegistry) != address(0), ZeroAddress()); + bondingRegistry = newBondingRegistry; } - /// @notice Updates the ciphernode registry contract address - /// @param newCiphernodeRegistry Address of the new ICiphernodeRegistry contract + /// @notice Updates the ciphernode registry contract + /// @param newCiphernodeRegistry The new ICiphernodeRegistry contract function setCiphernodeRegistry( - address newCiphernodeRegistry + ICiphernodeRegistry newCiphernodeRegistry ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(newCiphernodeRegistry != address(0), ZeroAddress()); - ciphernodeRegistry = ICiphernodeRegistry(newCiphernodeRegistry); + require(address(newCiphernodeRegistry) != address(0), ZeroAddress()); + ciphernodeRegistry = newCiphernodeRegistry; } - /// @notice Updates the Enclave contract address - /// @param newEnclave Address of the new IEnclave contract + /// @notice Updates the Enclave contract + /// @param newEnclave The new IEnclave contract function setEnclave( - address newEnclave + IEnclave newEnclave + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(address(newEnclave) != address(0), ZeroAddress()); + enclave = newEnclave; + } + + /// @inheritdoc ISlashingManager + function setE3RefundManager( + IE3RefundManager newRefundManager ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(newEnclave != address(0), ZeroAddress()); - enclave = IEnclave(newEnclave); + require(address(newRefundManager) != address(0), ZeroAddress()); + e3RefundManager = newRefundManager; } /// @inheritdoc ISlashingManager @@ -418,9 +430,11 @@ contract SlashingManager is ISlashingManager, AccessControl { SlashProposal storage p = _proposals[proposalId]; p.executed = true; + uint256 actualTicketSlashed = 0; + // Execute financial penalties if (p.ticketAmount > 0) { - bondingRegistry.slashTicketBalance( + actualTicketSlashed = bondingRegistry.slashTicketBalance( p.operator, p.ticketAmount, p.reason @@ -449,8 +463,35 @@ contract SlashingManager is ISlashingManager, AccessControl { // If active count drops below M, fail the E3 if (activeCount < thresholdM && p.failureReason > 0) { - // solhint-disable-next-line no-empty-blocks - try enclave.onE3Failed(p.e3Id, p.failureReason) {} catch {} + // NOTE: catch block must not be empty (solc optimizer bug, see below) + try enclave.onE3Failed(p.e3Id, p.failureReason) { + // Side effects occur in the external call + } catch { + // E3 already failed or other error — slash still proceeds + emit RoutingFailed(p.e3Id, 0); + } + } + } + + // Route slashed ticket funds to E3 refund pool for requester/honest node compensation. + // Uses self-call pattern for try/catch atomicity: if either the BondingRegistry redirect + // or the E3RefundManager accounting fails, both revert together and slashed funds remain + // in BondingRegistry for treasury withdrawal. The slash itself still proceeds. + if (actualTicketSlashed > 0) { + IEnclave.E3Stage stage = enclave.getE3Stage(p.e3Id); + if (stage == IEnclave.E3Stage.Failed) { + // NOTE: The catch block must not be empty — solc >=0.8.28 with + // optimizer enabled will eliminate the external call when both + // try and catch blocks are empty (compiler optimization bug). + try + this.routeSlashedFundsToRefund(p.e3Id, actualTicketSlashed) + { + // Side effects occur in the external call + } catch { + // Routing failed — slashed funds stay in BondingRegistry for + // treasury withdrawal. The slash itself still proceeds. + emit RoutingFailed(p.e3Id, actualTicketSlashed); + } } } @@ -465,6 +506,18 @@ contract SlashingManager is ISlashingManager, AccessControl { ); } + /// @inheritdoc ISlashingManager + /// @dev Atomically redirects slashed ticket funds from BondingRegistry to E3RefundManager + /// and updates the refund distribution. External with self-only access for try/catch. + function routeSlashedFundsToRefund(uint256 e3Id, uint256 amount) external { + require(msg.sender == address(this), Unauthorized()); + address refundManager = address(e3RefundManager); + require(refundManager != address(0), ZeroAddress()); + bondingRegistry.redirectSlashedTicketFunds(refundManager, amount); + enclave.routeSlashedFunds(e3Id, amount); + emit SlashedFundsRoutedToRefund(e3Id, amount); + } + // ====================== // Appeal Functions // ====================== diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index effca3bf47..afa35a4cc0 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -192,6 +192,9 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting CiphernodeRegistry address in SlashingManager..."); await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); + console.log("Setting E3RefundManager address in SlashingManager..."); + await slashingManager.setE3RefundManager(e3RefundManagerAddress); + console.log("Setting SlashingManager address in Enclave..."); await enclave.setSlashingManager(slashingManagerAddress); diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index 79db3af244..cd5ceb3f53 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -15,6 +15,7 @@ import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken" import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; import MockDecryptionVerifierModule from "../../ignition/modules/mockDecryptionVerifier"; import MockE3ProgramModule from "../../ignition/modules/mockE3Program"; +import MockCircuitVerifierModule from "../../ignition/modules/mockSlashingVerifier"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { @@ -23,9 +24,11 @@ import { E3RefundManager__factory as E3RefundManagerFactory, Enclave__factory as EnclaveFactory, EnclaveToken__factory as EnclaveTokenFactory, + MockCircuitVerifier__factory as MockCircuitVerifierFactory, MockDecryptionVerifier__factory as MockDecryptionVerifierFactory, MockE3Program__factory as MockE3ProgramFactory, MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, } from "../../types"; const { ethers, ignition, networkHelpers } = await network.connect(); @@ -70,6 +73,51 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const encryptionSchemeId = "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; + + // Slash-related constants for E2E tests + const REASON_BAD_PROOF = ethers.keccak256( + ethers.toUtf8Bytes("E3_BAD_PROOF"), + ); + const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ), + ); + + /** + * Helper to create a signed proof evidence bundle for proposeSlash. + */ + async function signAndEncodeProof( + signer: Signer, + e3Id: number, + verifierAddress: string, + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + chainId: number = 31337, + proofType: number = 0, + ): Promise { + const messageHash = ethers.keccak256( + abiCoder.encode( + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32"], + [ + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + ethers.keccak256(zkProof), + ethers.keccak256( + ethers.solidityPacked(["bytes32[]"], [publicInputs]), + ), + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], + ); + } + const setup = async () => { // ── Signers ──────────────────────────────────────────────────────────────── const [owner, requester, treasury, operator1, operator2, computeProvider] = @@ -211,8 +259,24 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { owner, ); + // ── Mock Circuit Verifier (for SlashingManager proof-based slashes) ──────── + const { mockCircuitVerifier } = await ignition.deploy( + MockCircuitVerifierModule, + ); + const circuitVerifier = MockCircuitVerifierFactory.connect( + await mockCircuitVerifier.getAddress(), + owner, + ); + + // ── SlashingManager typed factory ────────────────────────────────────────── + const slashingManagerTyped = SlashingManagerFactory.connect( + await slashingManager.getAddress(), + owner, + ); + // ── Wire Up Contracts ────────────────────────────────────────────────────── await enclave.setE3RefundManager(e3RefundManagerAddress); + await enclave.setSlashingManager(await slashingManager.getAddress()); await enclave.enableE3Program(await e3Program.getAddress()); await enclave.setDecryptionVerifier( encryptionSchemeId, @@ -229,6 +293,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); await slashingManager.setEnclave(enclaveAddress); + await slashingManager.setE3RefundManager(e3RefundManagerAddress); await registry.setEnclave(enclaveAddress); await registry.setBondingRegistry(await bondingRegistry.getAddress()); @@ -236,6 +301,19 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await enclaveTicketToken.setRegistry(await bondingRegistry.getAddress()); + // ── Slash Policy (for E2E routing tests) ─────────────────────────────────── + await slashingManagerTyped.setSlashPolicy(REASON_BAD_PROOF, { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await circuitVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 0, + }); + // ── Mint Tokens ──────────────────────────────────────────────────────────── await usdcToken.mint(requesterAddress, ethers.parseUnits("10000", 6)); await usdcToken.mint(e3RefundManagerAddress, ethers.parseUnits("10000", 6)); @@ -301,6 +379,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { e3RefundManager, bondingRegistry, registry, + slashingManager: slashingManagerTyped, + circuitVerifier, usdcToken, enclToken, e3Program, @@ -693,7 +773,188 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }); describe("Slashed Funds Routing", function () { - it("routes slashed funds 50/50 to requester and honest nodes", async function () { + it("E2E: slash via SlashingManager routes actual USDC to refund manager and requester can claim", async function () { + const { + enclave, + e3RefundManager, + registry, + slashingManager, + circuitVerifier, + bondingRegistry, + usdcToken, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Request E3, form committee, publish key + await makeRequest(); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + // 2. Wait past compute deadline → mark as failed + const e3 = await enclave.getE3(0); + const computeDeadline = + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow; + await time.increaseTo(computeDeadline + 1); + await enclave.markE3Failed(0); + + // 3. Process failure → distribution calculated, funds transferred to refund manager + await enclave.processE3Failure(0); + const distributionBefore = await e3RefundManager.getRefundDistribution(0); + expect(distributionBefore.calculated).to.be.true; + + // Record refund manager USDC balance before slash routing + const refundManagerBalanceBefore = await usdcToken.balanceOf( + await e3RefundManager.getAddress(), + ); + + // Record BondingRegistry's slashedTicketBalance before slash + const slashedBalanceBefore = await bondingRegistry.slashedTicketBalance(); + + // 4. Slash operator1 via proposeSlash (Lane A) — real on-chain flow + // This triggers: _executeSlash → slashTicketBalance → redirectSlashedTicketFunds + // → ticketToken.payout(refundManager, amount) → enclave.routeSlashedFunds → e3RefundManager.routeSlashedFunds + const proof = await signAndEncodeProof( + operator1, + 0, + await circuitVerifier.getAddress(), + ); + + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_PROOF, + proof, + ); + + // 5. Verify actual USDC moved to the refund manager + const refundManagerBalanceAfter = await usdcToken.balanceOf( + await e3RefundManager.getAddress(), + ); + const actualSlashedAmount = + refundManagerBalanceAfter - refundManagerBalanceBefore; + expect(actualSlashedAmount).to.be.gt(0); + + // Verify BondingRegistry's slashedTicketBalance was decremented + const slashedBalanceAfter = await bondingRegistry.slashedTicketBalance(); + expect(slashedBalanceAfter).to.equal( + slashedBalanceBefore, // slash added then redirect removed the same amount + ); + + // 6. Verify distribution was updated with requester-first priority + const distributionAfter = await e3RefundManager.getRefundDistribution(0); + expect(distributionAfter.totalSlashed).to.equal(actualSlashedAmount); + expect(distributionAfter.requesterAmount).to.be.gte( + distributionBefore.requesterAmount, + ); + + // 7. Verify requester can actually claim and receives the correct USDC + const requesterBalanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + await e3RefundManager.connect(requester).claimRequesterRefund(0); + const requesterBalanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + expect(requesterBalanceAfter - requesterBalanceBefore).to.equal( + distributionAfter.requesterAmount, + ); + }); + + it("E2E: honest nodes can claim their share after slashed funds are routed", async function () { + const { + enclave, + e3RefundManager, + registry, + slashingManager, + circuitVerifier, + usdcToken, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Request E3, form committee, publish key + await makeRequest(); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + // 2. Fail via compute timeout + const e3 = await enclave.getE3(0); + const computeDeadline = + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow; + await time.increaseTo(computeDeadline + 1); + await enclave.markE3Failed(0); + await enclave.processE3Failure(0); + + // 3. Record distribution BEFORE slash to verify it actually changes + const distributionBefore = await e3RefundManager.getRefundDistribution(0); + const honestNodeAmountBefore = distributionBefore.honestNodeAmount; + + // 4. Slash operator1 — this routes funds into the refund pool + const proof = await signAndEncodeProof( + operator1, + 0, + await circuitVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_PROOF, + proof, + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.honestNodeCount).to.be.gt(0); + // Verify that honestNodeAmount INCREASED due to slashed funds routing + expect(distribution.honestNodeAmount).to.be.gt(honestNodeAmountBefore); + expect(distribution.totalSlashed).to.be.gt(0); + + // 5. operator2 (honest node) claims their share + const op2BalanceBefore = await usdcToken.balanceOf( + await operator2.getAddress(), + ); + await e3RefundManager.connect(operator2).claimHonestNodeReward(0); + const op2BalanceAfter = await usdcToken.balanceOf( + await operator2.getAddress(), + ); + + const perNodeAmount = + distribution.honestNodeAmount / BigInt(distribution.honestNodeCount); + expect(op2BalanceAfter - op2BalanceBefore).to.equal(perNodeAmount); + }); + + it("requester-first priority: requester gets filled before honest nodes", async function () { const { enclave, e3RefundManager, @@ -709,7 +970,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await makeRequest(); - // Fail the E3 + // Fail the E3 at committee formation stage (no honest nodes, requester gets 95%) await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); await enclave.markE3Failed(0); await enclave.processE3Failure(0); @@ -717,8 +978,11 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distributionBefore = await e3RefundManager.getRefundDistribution(0); const slashedAmount = ethers.parseUnits("100", 6); - // Route slashed funds (normally called by SlashingManager through Enclave) - // For testing, temporarily set enclave to owner to call this permissioned function + // requesterGap = originalPayment - requesterAmount (how much more needed to be whole) + const requesterGap = + distributionBefore.originalPayment - distributionBefore.requesterAmount; + + // Route slashed funds via the enclave proxy (swap enclave address for test) const originalEnclave = await e3RefundManager.enclave(); await e3RefundManager.setEnclave(await owner.getAddress()); await e3RefundManager.connect(owner).routeSlashedFunds(0, slashedAmount); @@ -726,15 +990,65 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distributionAfter = await e3RefundManager.getRefundDistribution(0); - // Verify slashed funds are split 50/50 between requester and honest nodes + const expectedToRequester = + slashedAmount >= requesterGap ? requesterGap : slashedAmount; + const expectedToHonestNodes = slashedAmount - expectedToRequester; + expect(distributionAfter.requesterAmount).to.equal( - distributionBefore.requesterAmount + slashedAmount / 2n, + distributionBefore.requesterAmount + expectedToRequester, ); expect(distributionAfter.honestNodeAmount).to.equal( - distributionBefore.honestNodeAmount + slashedAmount / 2n, + distributionBefore.honestNodeAmount + expectedToHonestNodes, ); expect(distributionAfter.totalSlashed).to.equal(slashedAmount); }); + + it("queues slashed funds arriving before processE3Failure and applies on calculate", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + owner, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Fail E3 but DON'T call processE3Failure yet + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + + const slashedAmount = ethers.parseUnits("50", 6); + + // Route slashed funds BEFORE processE3Failure — should be queued + const originalEnclave = await e3RefundManager.enclave(); + await e3RefundManager.setEnclave(await owner.getAddress()); + await e3RefundManager.connect(owner).routeSlashedFunds(0, slashedAmount); + await e3RefundManager.setEnclave(originalEnclave); + + // Distribution should not exist yet + const distBefore = await e3RefundManager.getRefundDistribution(0); + expect(distBefore.calculated).to.be.false; + + // Now process the failure — pending funds should be applied + await enclave.processE3Failure(0); + + const distAfter = await e3RefundManager.getRefundDistribution(0); + expect(distAfter.calculated).to.be.true; + expect(distAfter.totalSlashed).to.equal(slashedAmount); + + // Invariant: all funds accounted for + expect( + distAfter.requesterAmount + + distAfter.honestNodeAmount + + distAfter.protocolAmount, + ).to.equal(distAfter.originalPayment + slashedAmount); + }); }); describe("Full Failure Flow - DKG Timeout", function () { diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts index cb1ee917df..6d6fa8f895 100644 --- a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -290,6 +290,7 @@ describe("Committee Expulsion & Fault Tolerance", function () { ); await slashingManager.setCiphernodeRegistry(registryAddress); await slashingManager.setEnclave(enclaveAddress); + await slashingManager.setE3RefundManager(addressOne); await ticketToken.setRegistry(await bondingRegistry.getAddress()); await usdcToken.mint(requesterAddress, ethers.parseUnits("100000", 6)); diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 8c2db3a6b2..c7ae3e9051 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -270,6 +270,8 @@ describe("SlashingManager", function () { ); await slashingManager.addSlasher(await slasher.getAddress()); await slashingManager.setCiphernodeRegistry(mockCiphernodeRegistryAddress); + await slashingManager.setEnclave(addressOne); + await slashingManager.setE3RefundManager(addressOne); // ── Return ───────────────────────────────────────────────────────────────── return { From 5cbb91129f8eb52118732758a0453dfa79ebe234 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 25 Feb 2026 18:32:27 +0500 Subject: [PATCH 15/21] fix: contract addresses --- examples/CRISP/enclave.config.yaml | 9 ++++++--- examples/CRISP/server/.env.example | 4 ++-- templates/default/enclave.config.yaml | 22 +++++++++++----------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index ada7040313..3803000e45 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,16 +3,19 @@ chains: rpc_url: ws://localhost:8545 contracts: e3_program: - address: '0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690' + address: '0x851356ae760d987E095750cCeb3bC6014560891C' deploy_block: 31 enclave: address: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' deploy_block: 16 ciphernode_registry: - address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' + address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' deploy_block: 14 bonding_registry: - address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' + address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' + deploy_block: 10 + slashing_manager: + address: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' deploy_block: 10 fee_token: address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 0cd31bbe47..42b1d6b4e1 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -14,8 +14,8 @@ CRON_API_KEY=1234567890 # Based on Default Anvil Deployments (Only for testing) ENCLAVE_ADDRESS="0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" -CIPHERNODE_REGISTRY_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" -E3_PROGRAM_ADDRESS="0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690" +CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +E3_PROGRAM_ADDRESS="0x851356ae760d987E095750cCeb3bC6014560891C" FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # E3 Config diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 661c745f2e..697b3c18b0 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,23 +3,23 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" + address: "0x851356ae760d987E095750cCeb3bC6014560891C" deploy_block: 1 # Set to actual deploy block enclave: - address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" - deploy_block: 1 # Set to actual deploy block + address: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' + deploy_block: 16 ciphernode_registry: - address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - deploy_block: 1 # Set to actual deploy block + address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' + deploy_block: 14 bonding_registry: - address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" - deploy_block: 1 # Set to actual deploy block + address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' + deploy_block: 10 slashing_manager: - address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" - deploy_block: 1 # Set to actual deploy block + address: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' + deploy_block: 10 fee_token: - address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - deploy_block: 1 # Set to actual deploy block + address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' + deploy_block: 6 program: dev: true From 1cfc287ad1ec422f7e3f6c88ce03ab75782dcebb Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 25 Feb 2026 18:33:22 +0500 Subject: [PATCH 16/21] fix: contract addresses --- .../CiphernodeRegistryOwnable.spec.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 9d6fec9fbe..a6b4c49581 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -27,7 +27,6 @@ import { const AddressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; -const AddressThree = "0x0000000000000000000000000000000000000003"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture } = networkHelpers; @@ -544,18 +543,18 @@ describe("CiphernodeRegistryOwnable", function () { it("reverts if the caller is not the owner", async function () { const { registry, notTheOwner } = await loadFixture(setup); await expect( - registry.connect(notTheOwner).addCiphernode(AddressThree), + registry.connect(notTheOwner).addCiphernode(AddressTwo), ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); }); it("adds the ciphernode to the registry", async function () { const { registry } = await loadFixture(setup); - expect(await registry.addCiphernode(AddressThree)); - expect(await registry.isEnabled(AddressThree)).to.be.true; + expect(await registry.addCiphernode(AddressTwo)); + expect(await registry.isEnabled(AddressTwo)).to.be.true; }); it("increments numCiphernodes", async function () { const { registry } = await loadFixture(setup); const numCiphernodes = await registry.numCiphernodes(); - expect(await registry.addCiphernode(AddressThree)); + expect(await registry.addCiphernode(AddressTwo)); expect(await registry.numCiphernodes()).to.equal( numCiphernodes + BigInt(1), ); @@ -564,10 +563,10 @@ describe("CiphernodeRegistryOwnable", function () { const { registry } = await loadFixture(setup); const treeSize = await registry.treeSize(); const numCiphernodes = await registry.numCiphernodes(); - await expect(await registry.addCiphernode(AddressThree)) + await expect(await registry.addCiphernode(AddressTwo)) .to.emit(registry, "CiphernodeAdded") .withArgs( - AddressThree, + AddressTwo, treeSize, numCiphernodes + BigInt(1), treeSize + BigInt(1), @@ -625,19 +624,19 @@ describe("CiphernodeRegistryOwnable", function () { it("reverts if the caller is not the owner", async function () { const { registry, notTheOwner } = await loadFixture(setup); await expect( - registry.connect(notTheOwner).setEnclave(AddressThree), + registry.connect(notTheOwner).setEnclave(AddressTwo), ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); }); it("sets the enclave address", async function () { const { registry } = await loadFixture(setup); - expect(await registry.setEnclave(AddressThree)); - expect(await registry.enclave()).to.equal(AddressThree); + expect(await registry.setEnclave(AddressTwo)); + expect(await registry.enclave()).to.equal(AddressTwo); }); it("emits an EnclaveSet event", async function () { const { registry } = await loadFixture(setup); - await expect(await registry.setEnclave(AddressThree)) + await expect(await registry.setEnclave(AddressTwo)) .to.emit(registry, "EnclaveSet") - .withArgs(AddressThree); + .withArgs(AddressTwo); }); }); @@ -700,7 +699,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("returns false if the ciphernode is not in the registry", async function () { const { registry } = await loadFixture(setup); - expect(await registry.isCiphernodeEligible(AddressThree)).to.be.false; + expect(await registry.isCiphernodeEligible(AddressTwo)).to.be.false; }); }); @@ -711,7 +710,7 @@ describe("CiphernodeRegistryOwnable", function () { }); it("returns false if the ciphernode is not currently enabled", async function () { const { registry } = await loadFixture(setup); - expect(await registry.isEnabled(AddressThree)).to.be.false; + expect(await registry.isEnabled(AddressTwo)).to.be.false; }); }); From 35f3b72ad078e54ea09df8b2bd95910b3921b5fc Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 25 Feb 2026 20:44:03 +0500 Subject: [PATCH 17/21] fix: contract addresses --- templates/default/enclave.config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 697b3c18b0..c84d8a4e2f 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -6,19 +6,19 @@ chains: address: "0x851356ae760d987E095750cCeb3bC6014560891C" deploy_block: 1 # Set to actual deploy block enclave: - address: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' + address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' deploy_block: 16 ciphernode_registry: - address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' + address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' deploy_block: 14 bonding_registry: - address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' + address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' deploy_block: 10 slashing_manager: - address: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' + address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' deploy_block: 10 fee_token: - address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' + address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' deploy_block: 6 program: From 14bdbdff3e211e9f360398833d18824fcf440895 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 25 Feb 2026 21:40:20 +0500 Subject: [PATCH 18/21] fix: contract addresses --- tests/integration/enclave.config.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 629a1bd097..056cef79cb 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -3,16 +3,19 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + address: "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690" deploy_block: 1 # Set to actual deploy block enclave: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" deploy_block: 1 # Set to actual deploy block ciphernode_registry: - address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block bonding_registry: - address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + address: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + deploy_block: 1 # Set to actual deploy block + slashing_manager: + address: "0x0165878A594ca255338adfa4d48449f69242Eb8F" deploy_block: 1 # Set to actual deploy block fee_token: address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" From 45f3e7905ee64dae88a960243e2d85bd94433e5e Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 26 Feb 2026 00:40:07 +0500 Subject: [PATCH 19/21] fix: route slashed funds after E3 completed --- .../IBondingRegistry.json | 2 +- .../ICiphernodeRegistry.json | 2 +- .../interfaces/IEnclave.sol/IEnclave.json | 40 ++-- .../ISlashingManager.json | 40 ++-- .../DkgPkVerifier.sol/DkgPkVerifier.json | 10 +- .../DkgPkVerifier.sol/ZKTranscriptLib.json | 2 +- .../contracts/E3RefundManager.sol | 61 ++++- .../enclave-contracts/contracts/Enclave.sol | 27 ++- .../contracts/interfaces/IE3RefundManager.sol | 27 ++- .../contracts/interfaces/IEnclave.sol | 14 +- .../contracts/interfaces/ISlashingManager.sol | 14 +- .../contracts/slashing/SlashingManager.sol | 35 +-- .../test/E3Lifecycle/E3Integration.spec.ts | 209 +++++++++++------- 13 files changed, 300 insertions(+), 183 deletions(-) diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index 5b76ab1824..e02d4d3f3f 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -946,5 +946,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" + "buildInfoId": "solc-0_8_28-8c7470711e670529b6d5512dcaf5896399b39260" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 3a91789b7f..a71ddb492b 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -787,5 +787,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" + "buildInfoId": "solc-0_8_28-8c7470711e670529b6d5512dcaf5896399b39260" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 47a26f89ee..4b6b181b37 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -449,7 +449,7 @@ "type": "uint256" } ], - "name": "SlashedFundsRouted", + "name": "SlashedFundsEscrowed", "type": "event" }, { @@ -571,6 +571,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "escrowSlashedFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "feeToken", @@ -1125,24 +1143,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "routeSlashedFunds", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -1263,5 +1263,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" + "buildInfoId": "solc-0_8_28-8c7470711e670529b6d5512dcaf5896399b39260" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json index 71e4ec2343..b477746915 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -427,7 +427,7 @@ "type": "uint256" } ], - "name": "SlashedFundsRoutedToRefund", + "name": "SlashedFundsEscrowedToRefund", "type": "event" }, { @@ -456,6 +456,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "escrowSlashedFundsToRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -782,24 +800,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "routeSlashedFundsToRefund", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -934,5 +934,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", - "buildInfoId": "solc-0_8_28-5311703a5d5fd8087d15f5c6957555cc4c2bf8c5" + "buildInfoId": "solc-0_8_28-e31106e21ee0b1a0de522e91c17cb752d4a7fb4e" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json index f9e20569ab..929cd00796 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "7752": [ + "32880": [ { "length": 32, "start": 91 @@ -152,13 +152,13 @@ "start": 11165 } ], - "7754": [ + "32882": [ { "length": 32, "start": 398 } ], - "7756": [ + "32884": [ { "length": 32, "start": 432 @@ -168,7 +168,7 @@ "start": 2303 } ], - "7758": [ + "32886": [ { "length": 32, "start": 3156 @@ -184,5 +184,5 @@ ] }, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-92904b17e0ba9daf009d56d318b68b455cca192e" + "buildInfoId": "solc-0_8_28-0b0bc0ce22be7ef0abc41e82d9a0bfe02856ddc5" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json index 6b18fd0a3d..b715329508 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json @@ -391,5 +391,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-92904b17e0ba9daf009d56d318b68b455cca192e" + "buildInfoId": "solc-0_8_28-0b0bc0ce22be7ef0abc41e82d9a0bfe02856ddc5" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index 8d0683e475..3895d21fae 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -52,7 +52,7 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses mapping(uint256 e3Id => address[] nodes) internal _honestNodes; - /// @notice Tracks pending slashed funds for E3s whose refund hasn't been calculated yet + /// @notice Pending slashed funds awaiting E3 terminal state mapping(uint256 e3Id => uint256 amount) internal _pendingSlashedFunds; //////////////////////////////////////////////////////////// // // @@ -98,7 +98,8 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { committeeFormationBps: 1000, dkgBps: 3000, decryptionBps: 5500, - protocolBps: 500 + protocolBps: 500, + successSlashedNodeBps: 5000 }); if (_owner != owner()) transferOwnership(_owner); @@ -325,23 +326,61 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { } /// @inheritdoc IE3RefundManager - function routeSlashedFunds( + function escrowSlashedFunds( uint256 e3Id, uint256 amount ) external onlyEnclave { require(amount > 0, "Zero amount"); RefundDistribution storage dist = _distributions[e3Id]; - if (!dist.calculated) { - // Distribution not yet calculated; queue for later application - // when processE3Failure -> calculateRefund is called. + if (dist.calculated) { + require(_claimCount[e3Id] == 0, "Claims already started"); + _applySlashedFunds(e3Id, amount); + } else { _pendingSlashedFunds[e3Id] += amount; - emit SlashedFundsRouted(e3Id, amount); - return; } - require(_claimCount[e3Id] == 0, "Claims already started"); - _applySlashedFunds(e3Id, amount); + emit SlashedFundsEscrowed(e3Id, amount); + } + + /// @inheritdoc IE3RefundManager + function distributeSlashedFundsOnSuccess( + uint256 e3Id, + address[] calldata honestNodes, + IERC20 paymentToken + ) external onlyEnclave { + uint256 escrowed = _pendingSlashedFunds[e3Id]; + if (escrowed == 0) return; + _pendingSlashedFunds[e3Id] = 0; + + require(address(paymentToken) != address(0), "Invalid fee token"); + + uint256 toNodes = (escrowed * _workAllocation.successSlashedNodeBps) / + 10000; + uint256 toProtocol = escrowed - toNodes; + + if (toProtocol > 0) { + paymentToken.safeTransfer(treasury, toProtocol); + } + + if (toNodes > 0 && honestNodes.length > 0) { + uint256 perNode = toNodes / honestNodes.length; + uint256 distributed = 0; + for (uint256 i = 0; i < honestNodes.length; i++) { + uint256 nodeAmount = perNode; + if (i == honestNodes.length - 1) { + nodeAmount = toNodes - distributed; + } + if (nodeAmount > 0) { + paymentToken.safeTransfer(honestNodes[i], nodeAmount); + } + distributed += nodeAmount; + } + } else if (toNodes > 0) { + paymentToken.safeTransfer(treasury, toNodes); + } + + emit SlashedFundsDistributedOnSuccess(e3Id, toNodes, toProtocol); } /// @notice Apply slashed funds to an E3's refund distribution @@ -365,7 +404,6 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { dist.honestNodeAmount += toHonestNodes; dist.totalSlashed += amount; - emit SlashedFundsRouted(e3Id, amount); emit SlashedFundsApplied(e3Id, toRequester, toHonestNodes); } @@ -412,6 +450,7 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { uint256(allocation.decryptionBps) + uint256(allocation.protocolBps); require(total == 10000, "Must sum to 10000"); + require(allocation.successSlashedNodeBps <= 10000, "Invalid BPS"); _workAllocation = allocation; diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 0d58672db6..017a15a722 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -501,16 +501,29 @@ contract Enclave is IEnclave, OwnableUpgradeable { uint256 totalAmount = e3Payments[e3Id]; e3Payments[e3Id] = 0; - if (totalAmount == 0) return; // Use the per-E3 fee token (not the global one, which may have been rotated) IERC20 paymentToken = _e3FeeTokens[e3Id]; + if (totalAmount == 0) { + e3RefundManager.distributeSlashedFundsOnSuccess( + e3Id, + activeNodes, + paymentToken + ); + return; + } + if (activeLength == 0) { address requester = _e3Requesters[e3Id]; if (requester != address(0)) { paymentToken.safeTransfer(requester, totalAmount); } + e3RefundManager.distributeSlashedFundsOnSuccess( + e3Id, + activeNodes, + paymentToken + ); return; } @@ -535,6 +548,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { paymentToken.forceApprove(address(bondingRegistry), 0); emit RewardsDistributed(e3Id, activeNodes, amounts); + + e3RefundManager.distributeSlashedFundsOnSuccess( + e3Id, + activeNodes, + paymentToken + ); } /// @notice Retrieves the honest committee nodes for a given E3. @@ -735,12 +754,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { } /// @inheritdoc IEnclave - function routeSlashedFunds( + function escrowSlashedFunds( uint256 e3Id, uint256 amount ) external onlySlashingManager { - e3RefundManager.routeSlashedFunds(e3Id, amount); - emit SlashedFundsRouted(e3Id, amount); + e3RefundManager.escrowSlashedFunds(e3Id, amount); + emit SlashedFundsEscrowed(e3Id, amount); } /// @inheritdoc IEnclave diff --git a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol index 6583bae279..1e83751616 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol @@ -24,6 +24,7 @@ interface IE3RefundManager { uint16 dkgBps; uint16 decryptionBps; uint16 protocolBps; + uint16 successSlashedNodeBps; } /// @notice Refund distribution for a failed E3 struct RefundDistribution { @@ -56,14 +57,20 @@ interface IE3RefundManager { uint256 amount, bytes32 claimType ); - /// @notice Emitted when slashed funds are routed to E3 - event SlashedFundsRouted(uint256 indexed e3Id, uint256 amount); - /// @notice Emitted when slashed funds are applied to refund distribution + /// @notice Emitted when slashed funds are escrowed for an E3 + event SlashedFundsEscrowed(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted when slashed funds are applied to a failed E3's refund distribution event SlashedFundsApplied( uint256 indexed e3Id, uint256 toRequester, uint256 toHonestNodes ); + /// @notice Emitted when escrowed slashed funds are distributed on success + event SlashedFundsDistributedOnSuccess( + uint256 indexed e3Id, + uint256 toNodes, + uint256 toProtocol + ); /// @notice Emitted when work allocation is updated event WorkAllocationUpdated(WorkValueAllocation allocation); //////////////////////////////////////////////////////////// @@ -117,10 +124,20 @@ interface IE3RefundManager { uint256 e3Id ) external returns (uint256 amount); - /// @notice Route slashed funds to E3 refund pool + /// @notice Escrow slashed funds — destination decided at terminal state /// @param e3Id The E3 ID /// @param amount The slashed amount - function routeSlashedFunds(uint256 e3Id, uint256 amount) external; + function escrowSlashedFunds(uint256 e3Id, uint256 amount) external; + + /// @notice Distribute escrowed slashed funds on success + /// @param e3Id The E3 ID + /// @param honestNodes Honest node addresses + /// @param paymentToken The fee token for this E3 + function distributeSlashedFundsOnSuccess( + uint256 e3Id, + address[] calldata honestNodes, + IERC20 paymentToken + ) external; /// @notice Get refund distribution for an E3 /// @param e3Id The E3 ID diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 5cdb473a03..533027674f 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -164,10 +164,10 @@ interface IEnclave { /// @param slashingManager The address of the SlashingManager contract. event SlashingManagerSet(address indexed slashingManager); - /// @notice Emitted when slashed funds are routed to E3 refund pool + /// @notice Emitted when slashed funds are escrowed for an E3 /// @param e3Id The E3 ID. - /// @param amount The amount of slashed funds routed. - event SlashedFundsRouted(uint256 indexed e3Id, uint256 amount); + /// @param amount The amount of slashed funds escrowed. + event SlashedFundsEscrowed(uint256 indexed e3Id, uint256 amount); /// @notice Emitted when a failed E3 is processed for refunds. /// @param e3Id The ID of the failed E3. @@ -377,11 +377,11 @@ interface IEnclave { /// @param reason The failure reason from FailureReason enum. function onE3Failed(uint256 e3Id, uint8 reason) external; - /// @notice Routes slashed ticket funds to the E3 refund pool - /// @dev Called by SlashingManager. Proxies to E3RefundManager.routeSlashedFunds. + /// @notice Escrow slashed funds for deferred distribution + /// @dev Called by SlashingManager. Proxies to E3RefundManager. /// @param e3Id The E3 ID. - /// @param amount Amount of slashed funds to route. - function routeSlashedFunds(uint256 e3Id, uint256 amount) external; + /// @param amount Amount of slashed funds to escrow. + function escrowSlashedFunds(uint256 e3Id, uint256 amount) external; //////////////////////////////////////////////////////////// // // diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index f28c0244bf..daecf707ce 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -261,11 +261,11 @@ interface ISlashingManager { ); /** - * @notice Emitted when slashed ticket funds are routed to the E3 refund pool + * @notice Emitted when slashed ticket funds are escrowed in the E3 refund pool * @param e3Id ID of the E3 computation - * @param amount Amount of slashed funds routed (underlying stablecoin) + * @param amount Amount of slashed funds escrowed (underlying stablecoin) */ - event SlashedFundsRoutedToRefund(uint256 indexed e3Id, uint256 amount); + event SlashedFundsEscrowedToRefund(uint256 indexed e3Id, uint256 amount); /** * @notice Emitted when routing slashed funds fails (funds remain in BondingRegistry) @@ -424,14 +424,14 @@ interface ISlashingManager { function executeSlash(uint256 proposalId) external; /** - * @notice Atomically redirects slashed ticket funds and updates E3 refund distribution + * @notice Atomically redirects slashed ticket funds to E3RefundManager escrow * @dev Only callable by this contract (self-call pattern for try/catch atomicity). * Transfers underlying stablecoin from BondingRegistry to E3RefundManager - * and calls Enclave.routeSlashedFunds to update the distribution. + * and calls Enclave.escrowSlashedFunds to update the escrow balance. * @param e3Id ID of the E3 computation - * @param amount Amount of slashed ticket balance to route + * @param amount Amount of slashed ticket balance to escrow */ - function routeSlashedFundsToRefund(uint256 e3Id, uint256 amount) external; + function escrowSlashedFundsToRefund(uint256 e3Id, uint256 amount) external; // ====================== // Appeal Functions diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 77cc3cd791..18f5d4113f 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -473,25 +473,14 @@ contract SlashingManager is ISlashingManager, AccessControl { } } - // Route slashed ticket funds to E3 refund pool for requester/honest node compensation. - // Uses self-call pattern for try/catch atomicity: if either the BondingRegistry redirect - // or the E3RefundManager accounting fails, both revert together and slashed funds remain - // in BondingRegistry for treasury withdrawal. The slash itself still proceeds. + // Escrow slashed ticket funds for deferred distribution. + // Self-call for try/catch atomicity — on failure, funds stay in BondingRegistry. if (actualTicketSlashed > 0) { - IEnclave.E3Stage stage = enclave.getE3Stage(p.e3Id); - if (stage == IEnclave.E3Stage.Failed) { - // NOTE: The catch block must not be empty — solc >=0.8.28 with - // optimizer enabled will eliminate the external call when both - // try and catch blocks are empty (compiler optimization bug). - try - this.routeSlashedFundsToRefund(p.e3Id, actualTicketSlashed) - { - // Side effects occur in the external call - } catch { - // Routing failed — slashed funds stay in BondingRegistry for - // treasury withdrawal. The slash itself still proceeds. - emit RoutingFailed(p.e3Id, actualTicketSlashed); - } + // NOTE: catch must not be empty — solc >=0.8.28 optimizer bug. + try + this.escrowSlashedFundsToRefund(p.e3Id, actualTicketSlashed) + {} catch { + emit RoutingFailed(p.e3Id, actualTicketSlashed); } } @@ -507,15 +496,15 @@ contract SlashingManager is ISlashingManager, AccessControl { } /// @inheritdoc ISlashingManager - /// @dev Atomically redirects slashed ticket funds from BondingRegistry to E3RefundManager - /// and updates the refund distribution. External with self-only access for try/catch. - function routeSlashedFundsToRefund(uint256 e3Id, uint256 amount) external { + /// @dev Atomically redirects slashed funds to E3RefundManager escrow. + /// External with self-only access for try/catch atomicity. + function escrowSlashedFundsToRefund(uint256 e3Id, uint256 amount) external { require(msg.sender == address(this), Unauthorized()); address refundManager = address(e3RefundManager); require(refundManager != address(0), ZeroAddress()); bondingRegistry.redirectSlashedTicketFunds(refundManager, amount); - enclave.routeSlashedFunds(e3Id, amount); - emit SlashedFundsRoutedToRefund(e3Id, amount); + enclave.escrowSlashedFunds(e3Id, amount); + emit SlashedFundsEscrowedToRefund(e3Id, amount); } // ====================== diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index cd5ceb3f53..41bbf67edb 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -75,9 +75,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; // Slash-related constants for E2E tests - const REASON_BAD_PROOF = ethers.keccak256( - ethers.toUtf8Bytes("E3_BAD_PROOF"), - ); + const REASON_BAD_PROOF = ethers.keccak256(ethers.toUtf8Bytes("E3_BAD_PROOF")); const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( ethers.toUtf8Bytes( "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", @@ -706,74 +704,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }); }); - describe("E3RefundManager Initialization", function () { - it("correctly sets enclave address", async function () { - const { enclave, e3RefundManager } = await loadFixture(setup); - - expect(await e3RefundManager.enclave()).to.equal( - await enclave.getAddress(), - ); - }); - }); - - describe("Full Failure Flow - Committee Formation Timeout", function () { - it("complete flow: request -> timeout -> fail -> process -> claim", async function () { - const { - enclave, - e3RefundManager, - makeRequest, - requester, - usdcToken, - operator1, - operator2, - setupOperator, - } = await loadFixture(setup); - - await setupOperator(operator1); - await setupOperator(operator2); - - // 1. Make request - await makeRequest(); - - // Verify stage - let stage = await enclave.getE3Stage(0); - expect(stage).to.equal(1); // Requested - - // 2. Fast forward past deadline - await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); - - // 3. Anyone can mark as failed - const [canFail, reason] = await enclave.checkFailureCondition(0); - expect(canFail).to.be.true; - expect(reason).to.equal(1); // CommitteeFormationTimeout - - await enclave.markE3Failed(0); - stage = await enclave.getE3Stage(0); - expect(stage).to.equal(6); // Failed - - // 4. Process failure - await enclave.processE3Failure(0); - - // 5. Requester claims refund - const balanceBefore = await usdcToken.balanceOf( - await requester.getAddress(), - ); - - await e3RefundManager.connect(requester).claimRequesterRefund(0); - - const balanceAfter = await usdcToken.balanceOf( - await requester.getAddress(), - ); - - const distribution = await e3RefundManager.getRefundDistribution(0); - expect(balanceAfter - balanceBefore).to.equal( - distribution.requesterAmount, - ); - }); - }); - - describe("Slashed Funds Routing", function () { - it("E2E: slash via SlashingManager routes actual USDC to refund manager and requester can claim", async function () { + describe("Slashed Funds Escrow", function () { + it("E2E: slash via SlashingManager escrows actual USDC to refund manager and requester can claim", async function () { const { enclave, e3RefundManager, @@ -829,7 +761,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { // 4. Slash operator1 via proposeSlash (Lane A) — real on-chain flow // This triggers: _executeSlash → slashTicketBalance → redirectSlashedTicketFunds - // → ticketToken.payout(refundManager, amount) → enclave.routeSlashedFunds → e3RefundManager.routeSlashedFunds + // → ticketToken.payout(refundManager, amount) → enclave.escrowSlashedFunds → e3RefundManager.escrowSlashedFunds const proof = await signAndEncodeProof( operator1, 0, @@ -877,7 +809,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { ); }); - it("E2E: honest nodes can claim their share after slashed funds are routed", async function () { + it("E2E: honest nodes can claim their share after slashed funds are escrowed", async function () { const { enclave, e3RefundManager, @@ -936,7 +868,7 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const distribution = await e3RefundManager.getRefundDistribution(0); expect(distribution.honestNodeCount).to.be.gt(0); - // Verify that honestNodeAmount INCREASED due to slashed funds routing + // Verify that honestNodeAmount INCREASED due to slashed funds escrow expect(distribution.honestNodeAmount).to.be.gt(honestNodeAmountBefore); expect(distribution.totalSlashed).to.be.gt(0); @@ -982,10 +914,10 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const requesterGap = distributionBefore.originalPayment - distributionBefore.requesterAmount; - // Route slashed funds via the enclave proxy (swap enclave address for test) + // Escrow slashed funds via the enclave proxy (swap enclave address for test) const originalEnclave = await e3RefundManager.enclave(); await e3RefundManager.setEnclave(await owner.getAddress()); - await e3RefundManager.connect(owner).routeSlashedFunds(0, slashedAmount); + await e3RefundManager.connect(owner).escrowSlashedFunds(0, slashedAmount); await e3RefundManager.setEnclave(originalEnclave); const distributionAfter = await e3RefundManager.getRefundDistribution(0); @@ -1025,10 +957,10 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { const slashedAmount = ethers.parseUnits("50", 6); - // Route slashed funds BEFORE processE3Failure — should be queued + // Escrow slashed funds BEFORE processE3Failure — should be queued const originalEnclave = await e3RefundManager.enclave(); await e3RefundManager.setEnclave(await owner.getAddress()); - await e3RefundManager.connect(owner).routeSlashedFunds(0, slashedAmount); + await e3RefundManager.connect(owner).escrowSlashedFunds(0, slashedAmount); await e3RefundManager.setEnclave(originalEnclave); // Distribution should not exist yet @@ -1427,6 +1359,127 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { }); describe("Success Path (Complete E3)", function () { + it("distributes escrowed slashed funds to nodes and treasury on successful completion", async function () { + const { + enclave, + e3RefundManager, + registry, + slashingManager, + circuitVerifier, + usdcToken, + makeRequest, + operator1, + operator2, + treasury, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Request E3, form committee, publish key + await makeRequest(); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + expect(await enclave.getE3Stage(0)).to.equal(3); // KeyPublished + + // 2. Slash operator1 during active E3 (before completion) + // With the stage-check removed, this should escrow funds in E3RefundManager + const refundManagerAddress = await e3RefundManager.getAddress(); + const refundBalanceBefore = + await usdcToken.balanceOf(refundManagerAddress); + + const proof = await signAndEncodeProof( + operator1, + 0, + await circuitVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_PROOF, + proof, + ); + + // Verify USDC moved to refund manager (escrowed) + const refundBalanceAfter = + await usdcToken.balanceOf(refundManagerAddress); + const actualSlashedAmount = refundBalanceAfter - refundBalanceBefore; + expect(actualSlashedAmount).to.be.gt(0); + + // 3. Complete the E3 successfully: publish ciphertext → publish plaintext + const e3 = await enclave.getE3(0); + await time.increaseTo(Number(e3.inputWindow[1])); + + const ciphertextOutput = "0x" + "ab".repeat(100); + const proofBytes = "0x1337"; + await enclave.publishCiphertextOutput(0, ciphertextOutput, proofBytes); + expect(await enclave.getE3Stage(0)).to.equal(4); // CiphertextReady + + // Record the E3 payment (normal rewards) before completion zeroes it + const e3Payment = await enclave.e3Payments(0); + + // Record balances before plaintext publish (which triggers _distributeRewards) + const treasuryAddress = await treasury.getAddress(); + const treasuryBalanceBefore = await usdcToken.balanceOf(treasuryAddress); + const op1BalanceBefore = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const op2BalanceBefore = await usdcToken.balanceOf( + await operator2.getAddress(), + ); + + const plaintextOutput = "0x" + "cd".repeat(100); + await enclave.publishPlaintextOutput(0, plaintextOutput, proofBytes); + expect(await enclave.getE3Stage(0)).to.equal(5); // Complete + + // 4. Verify escrowed slashed funds were distributed + // 50% to honest nodes (split equally), 50% to treasury + const expectedSlashedToNodes = + (actualSlashedAmount * BigInt(5000)) / BigInt(10000); + const expectedSlashedToTreasury = + actualSlashedAmount - expectedSlashedToNodes; + + const treasuryBalanceAfter = await usdcToken.balanceOf(treasuryAddress); + + // Treasury receives only the slashed-funds protocol share on success path + // (normal E3 rewards go entirely to nodes via bondingRegistry.distributeRewards) + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.equal( + expectedSlashedToTreasury, + ); + + // Honest nodes receive: normal E3 rewards (via bondingRegistry.distributeRewards) + // + slashed-funds node share (via distributeSlashedFundsOnSuccess). + // Both transfer directly to node addresses. + const op1BalanceAfter = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const op2BalanceAfter = await usdcToken.balanceOf( + await operator2.getAddress(), + ); + const nodesReceivedTotal = + op1BalanceAfter - + op1BalanceBefore + + (op2BalanceAfter - op2BalanceBefore); + expect(nodesReceivedTotal).to.equal(e3Payment + expectedSlashedToNodes); + + // Verify refund manager escrowed balance was drained + const refundBalanceFinal = + await usdcToken.balanceOf(refundManagerAddress); + expect(refundBalanceFinal).to.be.lt(refundBalanceAfter); + }); + it("transitions through all stages to completion", async function () { const { enclave, From d661b012f6a93ff73a990fee6325caee2f5a0822 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 26 Feb 2026 17:10:06 +0500 Subject: [PATCH 20/21] fix: contract addresses --- templates/default/enclave.config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 9344c01a39..c113519398 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,22 +3,22 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0x851356ae760d987E095750cCeb3bC6014560891C" + address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" deploy_block: 1 # Set to actual deploy block enclave: - address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' + address: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' deploy_block: 16 ciphernode_registry: - address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' + address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' deploy_block: 14 bonding_registry: - address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' + address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' deploy_block: 10 slashing_manager: - address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' + address: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' deploy_block: 10 fee_token: - address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' + address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' deploy_block: 6 program: From 430398c558c2f7a5188273dbeeb0741c9ee1ec3a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Fri, 27 Feb 2026 00:40:29 +0500 Subject: [PATCH 21/21] fix: review comments --- .../contracts/E3RefundManager.sol | 3 ++- .../contracts/registry/BondingRegistry.sol | 9 +++++++-- .../registry/CiphernodeRegistryOwnable.sol | 18 ++++-------------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index 3895d21fae..1e216be75d 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -384,7 +384,8 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { } /// @notice Apply slashed funds to an E3's refund distribution - /// @dev Priority: make requester whole first, then distribute remainder to honest nodes. + /// @dev This function is ONLY called on the failure path.Priority: make requester whole first, + /// then distribute remainder to honest nodes. /// The requester is filled up to their original E3 payment before honest nodes receive /// any portion, ensuring the party who paid for the computation is compensated first. /// @param e3Id The E3 ID diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 924461c823..fbc45c66a7 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -131,6 +131,12 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { _; } + /// @dev Restricts function access to authorized reward distributors + modifier onlyAuthorizedDistributor() { + require(authorizedDistributors[msg.sender], OnlyRewardDistributor()); + _; + } + /// @dev Reverts if operator has an exit in progress that hasn't unlocked yet /// @param operator Address of the operator to check modifier noExitInProgress(address operator) { @@ -592,8 +598,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { IERC20 rewardToken, address[] calldata recipients, uint256[] calldata amounts - ) external { - require(authorizedDistributors[msg.sender], OnlyRewardDistributor()); + ) external onlyAuthorizedDistributor { require(recipients.length == amounts.length, ArrayLengthMismatch()); uint256 len = recipients.length; diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index ebbe05eef0..d5a110400d 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -276,10 +276,6 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { ) external onlyOwner { Committee storage c = committees[e3Id]; - require( - c.stage != ICiphernodeRegistry.CommitteeStage.None, - CommitteeNotRequested() - ); require( c.stage == ICiphernodeRegistry.CommitteeStage.Finalized, CommitteeNotFinalized() @@ -413,16 +409,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } c.stage = ICiphernodeRegistry.CommitteeStage.Finalized; - // Mark every finalized member as Active. - // Active → counts toward viability, earns rewards. - // Expelled → was a member (enables re-slash via Lane A) but no longer counts. - uint256 committeeLen = c.topNodes.length; - for (uint256 i = 0; i < committeeLen; ++i) { - c.memberStatus[c.topNodes[i]] = ICiphernodeRegistry - .MemberStatus - .Active; - } - c.activeCount = committeeLen; + c.activeCount = c.topNodes.length; enclave.onCommitteeFinalized(e3Id); emit CommitteeFinalized(e3Id, c.topNodes); @@ -742,6 +729,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { if (top.length < cap) { top.push(node); c.scoreOf[node] = score; + c.memberStatus[node] = ICiphernodeRegistry.MemberStatus.Active; return true; } @@ -757,8 +745,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { if (score >= worstScore) return false; + c.memberStatus[top[worstIdx]] = ICiphernodeRegistry.MemberStatus.None; top[worstIdx] = node; c.scoreOf[node] = score; + c.memberStatus[node] = ICiphernodeRegistry.MemberStatus.Active; return true; }