// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { TestBase } from "./TestBase.sol"; import { ConsentRegistryV3 } from "../src/ConsentRegistryV3.sol"; import { SessionCapabilitiesV3 } from "../src/SessionCapabilitiesV3.sol"; import { PortableReceiptsV3 } from "../src/PortableReceiptsV3.sol"; import { CrossChainReceiptRegistryV3, ICrossChainReceiptVerifierV3 } from "../src/CrossChainReceiptRegistryV3.sol"; import { VNSV3 } from "../src/VNSV3.sol"; import { GovernanceControlsV3 } from "../src/common/GovernanceControlsV3.sol"; contract SessionTargetMock { uint256 public calls; address public lastCaller; uint256 public lastValue; address public authority = address(0xA0); bytes32 public configHash = keccak256("session-target-config/v1"); function permitted(uint256 value) external returns (uint256) { calls++; lastCaller = msg.sender; lastValue = value; return value + 1; } function forbidden() external { calls++; } function capabilityTargetConfig() external view returns (address, address, bytes32) { return (authority, address(0), configHash); } } contract ReceiptVerifierMock is ICrossChainReceiptVerifierV3 { function verifyReceipt( uint256 sourceChainId, address sourceRegistry, uint256 destinationChainId, address destinationRegistry, bytes32 receiptId, bytes32 payloadHash, bytes calldata proof ) external pure returns (bool) { return sourceChainId == 1 && sourceRegistry == address(0x1111) && destinationChainId != sourceChainId && destinationRegistry != address(0) && receiptId != bytes32(0) && payloadHash != bytes32(0) && keccak256(proof) == keccak256("valid-proof"); } } contract AuthorizationAndReceiptsTest is TestBase { uint256 internal constant PRINCIPAL_KEY = 0xA11CE; uint256 internal constant ISSUER_KEY = 0xB0B; address internal principal; address internal issuer; address internal constant SESSION = address(0x5001); address internal constant SUBJECT = address(0x5002); ConsentRegistryV3 internal consents; SessionCapabilitiesV3 internal sessions; PortableReceiptsV3 internal receipts; SessionTargetMock internal target; function setUp() public { principal = vm.addr(PRINCIPAL_KEY); issuer = vm.addr(ISSUER_KEY); consents = new ConsentRegistryV3(); sessions = new SessionCapabilitiesV3(address(0x1001), address(0x1002), 1 days); receipts = new PortableReceiptsV3(); target = new SessionTargetMock(); } function testConsentSignatureReplayAndRevocation() public { ConsentRegistryV3.Consent memory consent = ConsentRegistryV3.Consent({ principal: principal, delegate: SESSION, scope: keccak256("scope"), purpose: keccak256("purpose"), notBefore: uint64(block.timestamp), notAfter: uint64(block.timestamp + 7 days), nonce: 0 }); bytes32 structHash = keccak256( abi.encode( consents.CONSENT_TYPEHASH(), consent.principal, consent.delegate, consent.scope, consent.purpose, consent.notBefore, consent.notAfter, consent.nonce ) ); bytes memory signature = _signature(PRINCIPAL_KEY, keccak256(abi.encodePacked("\x19\x01", consents.DOMAIN_SEPARATOR(), structHash))); bytes32 id = consents.registerBySignature(consent, signature); assertTrue(consents.isActive(id)); vm.expectRevert(ConsentRegistryV3.BadNonce.selector); consents.registerBySignature(consent, signature); vm.prank(principal); consents.revoke(id); assertFalse(consents.isActive(id)); } function testConsentDomainSeparationRejectsCrossContractReplay() public { ConsentRegistryV3 other = new ConsentRegistryV3(); ConsentRegistryV3.Consent memory consent = ConsentRegistryV3.Consent({ principal: principal, delegate: SESSION, scope: keccak256("scope"), purpose: keccak256("purpose"), notBefore: uint64(block.timestamp), notAfter: uint64(block.timestamp + 1 days), nonce: 0 }); bytes32 structHash = keccak256( abi.encode( consents.CONSENT_TYPEHASH(), consent.principal, consent.delegate, consent.scope, consent.purpose, consent.notBefore, consent.notAfter, consent.nonce ) ); bytes memory signature = _signature(PRINCIPAL_KEY, keccak256(abi.encodePacked("\x19\x01", consents.DOMAIN_SEPARATOR(), structHash))); vm.expectRevert(); other.registerBySignature(consent, signature); } function testSessionSelectorLimitExpiryAndRevocation() public { SessionCapabilitiesV3.Capability memory capability = SessionCapabilitiesV3.Capability({ principal: principal, session: SESSION, chainId: block.chainid, target: address(target), targetCodehash: address(target).codehash, implementation: address(0), implementationCodehash: bytes32(0), targetAuthority: target.authority(), selector: target.permitted.selector, callDataHash: keccak256(abi.encodeCall(target.permitted, (41))), callDataSchemaHash: keccak256("permitted(uint256):exact/v1"), targetConfigHash: target.configHash(), callValue: 0, maxCalls: 2, notBefore: uint64(block.timestamp), notAfter: uint64(block.timestamp + 1 days), nonce: 0 }); vm.prank(principal); bytes32 id = sessions.registerDirect(capability); vm.prank(SESSION); bytes memory result = sessions.execute(id, abi.encodeCall(target.permitted, (41))); assertEq(abi.decode(result, (uint256)), 42); assertEq(target.lastCaller(), address(sessions)); vm.expectRevert(SessionCapabilitiesV3.WrongCall.selector); vm.prank(SESSION); sessions.execute(id, abi.encodeCall(target.forbidden, ())); vm.prank(principal); sessions.revoke(id); vm.expectRevert(SessionCapabilitiesV3.CapabilityInactive.selector); vm.prank(SESSION); sessions.execute(id, abi.encodeCall(target.permitted, (1))); } function testPortableReceiptSignatureAndDuplicateProtection() public { PortableReceiptsV3.Receipt memory receipt = PortableReceiptsV3.Receipt({ issuer: issuer, subject: bytes32(uint256(uint160(SUBJECT))), schema: keccak256("schema"), payloadHash: keccak256("payload"), provenanceHash: keccak256("ipfs-or-content-address"), consentId: keccak256("consent-id"), issuedAt: uint64(block.timestamp), notAfter: uint64(block.timestamp + 30 days), nonce: 0 }); bytes32 structHash = keccak256( abi.encode( receipts.RECEIPT_TYPEHASH(), receipt.issuer, receipt.subject, receipt.schema, receipt.payloadHash, receipt.provenanceHash, receipt.consentId, receipt.issuedAt, receipt.notAfter, receipt.nonce ) ); bytes memory signature = _signature(ISSUER_KEY, keccak256(abi.encodePacked("\x19\x01", receipts.DOMAIN_SEPARATOR(), structHash))); bytes32 id = receipts.registerBySignature(receipt, signature); assertTrue(receipts.receiptDigest(id) != bytes32(0)); vm.expectRevert(PortableReceiptsV3.BadNonce.selector); receipts.registerBySignature(receipt, signature); } function testCrossChainReceiptBindsRouteAndRejectsDuplicate() public { ReceiptVerifierMock verifier = new ReceiptVerifierMock(); CrossChainReceiptRegistryV3 registry = new CrossChainReceiptRegistryV3(1, address(0x1111), address(verifier), address(verifier).codehash); bytes32 id = keccak256("receipt"); bytes32 payload = keccak256("payload"); registry.importReceipt(id, payload, bytes("valid-proof")); assertEq(registry.importedPayloadHash(id), payload); vm.expectRevert(CrossChainReceiptRegistryV3.DuplicateReceipt.selector); registry.importReceipt(id, payload, bytes("valid-proof")); } function testVNSCommitRevealExpiryAndRecoveryDelay() public { VNSV3 vns = new VNSV3(365 days, 7 days, 2, 100); bytes32 label = keccak256("alice"); bytes32 secret = keccak256("secret"); bytes32 commitment = vns.makeCommitment(label, principal, secret, 30 days); vm.prank(principal); vns.commit(commitment); vm.expectRevert(VNSV3.InvalidCommitment.selector); vm.prank(principal); vns.register(label, secret, 30 days); vm.roll(block.number + 2); vm.prank(principal); vns.register(label, secret, 30 days); assertEq(vns.resolve(label), principal); vm.prank(principal); vns.setRecovery(label, SESSION); vm.expectRevert(VNSV3.RecoveryNotReady.selector); vm.prank(SESSION); vns.recover(label, SUBJECT); vm.warp(block.timestamp + 7 days); vm.prank(SESSION); vns.recover(label, SUBJECT); assertEq(vns.resolve(label), SUBJECT); } function testVNSCopiedCommitmentCannotBlockVictimOrReveal() public { VNSV3 vns = new VNSV3(365 days, 7 days, 2, 100); bytes32 label = keccak256("copy-resistant"); bytes32 secret = keccak256("victim-secret"); bytes32 commitment = vns.makeCommitment(label, principal, secret, 30 days); vm.prank(SUBJECT); vns.commit(commitment); vm.prank(principal); vns.commit(commitment); assertTrue( vns.commitmentBlock(vns.commitmentKey(commitment, SUBJECT)) == vns.commitmentBlock(vns.commitmentKey(commitment, principal)) ); vm.roll(block.number + 2); vm.expectRevert(VNSV3.InvalidCommitment.selector); vm.prank(SUBJECT); vns.register(label, secret, 30 days); vm.prank(principal); vns.register(label, secret, 30 days); assertEq(vns.resolve(label), principal); } function testTwoStepGovernanceProposeAcceptCancelAndOldAuthorityLoss() public { address oldGovernance = address(0x1001); address guardian = address(0x1002); address nextGovernance = address(0x1003); address cancelled = address(0x1004); vm.expectRevert(GovernanceControlsV3.GovernanceUnauthorized.selector); sessions.proposeGovernance(nextGovernance); vm.prank(oldGovernance); sessions.proposeGovernance(nextGovernance); vm.expectRevert(GovernanceControlsV3.GovernanceUnauthorized.selector); sessions.acceptGovernance(); vm.prank(nextGovernance); vm.warp(block.timestamp + sessions.AUTHORITY_CHANGE_DELAY()); vm.prank(nextGovernance); sessions.acceptGovernance(); assertEq(sessions.governance(), nextGovernance); vm.expectRevert(GovernanceControlsV3.GovernanceUnauthorized.selector); vm.prank(oldGovernance); sessions.proposeGovernance(cancelled); vm.prank(nextGovernance); sessions.proposeGovernance(cancelled); vm.prank(nextGovernance); sessions.cancelGovernanceTransfer(); assertEq(sessions.pendingGovernance(), address(0)); vm.prank(nextGovernance); sessions.proposeGovernance(cancelled); vm.prank(guardian); sessions.emergencyRevokeGovernanceTransfer(); assertEq(sessions.pendingGovernance(), address(0)); assertEq(sessions.governance(), nextGovernance); } function testGovernanceEmergencyVetoCannotInstallOrReplaceGovernance() public { address guardian = address(0x1002); address proposed = address(0x1003); vm.prank(address(0x1001)); sessions.proposeGovernance(proposed); vm.expectRevert(GovernanceControlsV3.GovernanceUnauthorized.selector); vm.prank(guardian); sessions.acceptGovernance(); vm.prank(guardian); sessions.emergencyRevokeGovernanceTransfer(); vm.expectRevert(GovernanceControlsV3.NoPendingGovernance.selector); vm.prank(proposed); sessions.acceptGovernance(); assertEq(sessions.governance(), address(0x1001)); } }