Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/safe-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ These integration tests live under their own profile because the Safe contracts

## Deploy the guard

The deployment script reads the registry and fail-open threshold from the environment. Foundry handles the deployer wallet; the script does not read a private key.
The deployment script reads the registry, fail-open threshold, and initial protocol manager from the environment. Foundry handles the deployer wallet; the script does not read a private key.

```sh
FOUNDRY_PROFILE=safe-guard \
CREDIBLE_REGISTRY=0x... \
FAIL_OPEN_BLOCK_THRESHOLD=75 \
INITIAL_PROTOCOL_MANAGER=0x... \
forge script examples/safe-guard/script/DeployCredibleSafeGuard.s.sol \
--rpc-url "$RPC_URL" \
--account <foundry-keystore-account> \
Expand Down
11 changes: 8 additions & 3 deletions examples/safe-guard/script/DeployCredibleSafeGuard.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,29 @@ contract DeployCredibleSafeGuard is Script {
function run() external returns (CredibleSafeGuard guard) {
address registry = vm.envAddress("CREDIBLE_REGISTRY");
uint256 threshold = vm.envUint("FAIL_OPEN_BLOCK_THRESHOLD");
address protocolManager = vm.envAddress("INITIAL_PROTOCOL_MANAGER");

// Validate the registry before broadcasting so a misconfigured (codeless or
// non-responsive) registry is caught up front rather than silently deploying a
// permanently-fail-open guard.
validateRegistry(registry);

vm.startBroadcast();
Comment thread
makemake-kbo marked this conversation as resolved.
guard = deploy(registry, threshold);
guard = deploy(registry, threshold, protocolManager);
vm.stopBroadcast();

console2.log("Chain ID:", block.chainid);
console2.log("Credible Safe guard:", address(guard));
console2.log("Credible Registry:", registry);
console2.log("Fail-open block threshold:", threshold);
console2.log("Initial protocol manager:", protocolManager);
}

function deploy(address registry, uint256 threshold) public returns (CredibleSafeGuard) {
return new CredibleSafeGuard(ICredibleRegistry(registry), threshold);
function deploy(address registry, uint256 threshold, address protocolManager)
public
returns (CredibleSafeGuard)
{
return new CredibleSafeGuard(ICredibleRegistry(registry), threshold, protocolManager);
}

/// @notice Asserts the registry has code and answers both reads the guard depends on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contract CredibleSafeGuardSafeIntegrationTest is Test {

uint256 internal constant THRESHOLD = 75;
uint256 internal constant BASE_BLOCK = 1_000_000;
address internal constant PROTOCOL_MANAGER = address(0xA11CE);

uint256 internal ownerPk = uint256(keccak256("safe.owner"));
address internal owner;
Expand All @@ -32,7 +33,7 @@ contract CredibleSafeGuardSafeIntegrationTest is Test {
owner = vm.addr(ownerPk);

registry = new CredibleRegistryMock();
guard = new CredibleSafeGuard(registry, THRESHOLD);
guard = new CredibleSafeGuard(registry, THRESHOLD, PROTOCOL_MANAGER);

Safe singleton = new Safe();
SafeProxyFactory factory = new SafeProxyFactory();
Expand Down
25 changes: 16 additions & 9 deletions examples/safe-guard/test/CredibleSafeGuardScripts.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {SafeProxyFactory} from "../../../lib/safe-smart-account/contracts/proxie
import {Enum} from "../../../lib/safe-smart-account/contracts/common/Enum.sol";

import {CredibleSafeGuard} from "credible-std/protection/safe/CredibleSafeGuard.sol";
import {InitialProtocolManager} from
"credible-std/protection/credible_block/InitialProtocolManager.sol";
import {CredibleRegistryMock} from "../src/CredibleRegistryMock.sol";
import {DeployCredibleSafeGuard} from "../script/DeployCredibleSafeGuard.s.sol";
import {GenerateSafeGuardBatch} from "../script/GenerateSafeGuardBatch.s.sol";
Expand Down Expand Up @@ -39,6 +41,7 @@ contract CredibleSafeGuardScriptsTest is Test {

uint256 internal constant THRESHOLD = 75;
uint256 internal constant CREATED_AT = 1_700_000_000_000;
address internal constant PROTOCOL_MANAGER = address(0xA11CE);

uint256 internal ownerPk = uint256(keccak256("safe.owner"));
address internal owner;
Expand Down Expand Up @@ -66,18 +69,22 @@ contract CredibleSafeGuardScriptsTest is Test {
}

function test_deployHelper_deploysConfiguredGuard() public {
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);

assertEq(address(guard.credibleRegistry()), address(registry));
assertEq(guard.failOpenBlockThreshold(), THRESHOLD);
assertEq(guard.initialProtocolManager(), PROTOCOL_MANAGER);
}

function test_deployHelper_preservesConstructorValidation() public {
vm.expectRevert(CredibleSafeGuard.ZeroCredibleRegistryAddress.selector);
deployer.deploy(address(0), THRESHOLD);
deployer.deploy(address(0), THRESHOLD, PROTOCOL_MANAGER);

vm.expectRevert(CredibleSafeGuard.ZeroFailOpenBlockThreshold.selector);
deployer.deploy(address(registry), 0);
deployer.deploy(address(registry), 0, PROTOCOL_MANAGER);

vm.expectRevert(InitialProtocolManager.ZeroInitialProtocolManager.selector);
deployer.deploy(address(registry), THRESHOLD, address(0));
}

function test_validateRegistry_acceptsResponsiveRegistry() public view {
Expand Down Expand Up @@ -118,7 +125,7 @@ contract CredibleSafeGuardScriptsTest is Test {
}

function test_installBatch_matchesSafeTransactionBuilderSchema() public {
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);
string memory json = generator.buildInstallBatch(address(safe), address(guard), block.chainid, CREATED_AT);

assertEq(vm.parseJsonString(json, ".version"), "1.0");
Expand Down Expand Up @@ -158,16 +165,16 @@ contract CredibleSafeGuardScriptsTest is Test {
}

function test_generatedInstallBatch_executesThroughRealSafe() public {
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);
string memory json = generator.buildInstallBatch(address(safe), address(guard), block.chainid, CREATED_AT);

_executeGeneratedBatch(json);
assertEq(_installedGuard(), address(guard));
}

function test_generatedBatch_replacesAndRemovesGuard() public {
CredibleSafeGuard firstGuard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard secondGuard = deployer.deploy(address(registry), THRESHOLD + 1);
CredibleSafeGuard firstGuard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);
CredibleSafeGuard secondGuard = deployer.deploy(address(registry), THRESHOLD + 1, PROTOCOL_MANAGER);

_executeGeneratedBatch(
generator.buildInstallBatch(address(safe), address(firstGuard), block.chainid, CREATED_AT)
Expand All @@ -186,7 +193,7 @@ contract CredibleSafeGuardScriptsTest is Test {
}

function test_rejectsInvalidSafeAndGuardInputs() public {
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);

vm.expectRevert(SafeGuardBatch.ZeroSafeAddress.selector);
generator.buildInstallBatch(address(0), address(guard), block.chainid, CREATED_AT);
Expand Down Expand Up @@ -216,7 +223,7 @@ contract CredibleSafeGuardScriptsTest is Test {
}

function test_run_writesImportableInstallBatch() public {
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD);
CredibleSafeGuard guard = deployer.deploy(address(registry), THRESHOLD, PROTOCOL_MANAGER);
vm.setEnv("SAFE_ADDRESS", vm.toString(address(safe)));
vm.setEnv("SAFE_GUARD_ACTION", "install");
vm.setEnv("CREDIBLE_SAFE_GUARD", vm.toString(address(guard)));
Expand Down
24 changes: 24 additions & 0 deletions src/protection/credible_block/IInitialProtocolManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/// @title IInitialProtocolManager
/// @author Phylax Systems
/// @notice Interface a protected contract exposes to declare the address allowed to manage its
/// assertions in the Credible Layer. Every protected contract needs a protocol manager;
/// exposing the intended manager on the contract itself lets the state oracle set it
/// without a manual review round.
/// @dev The state oracle calls {initialProtocolManager} on the contract when the protocol is
/// initialized in the Credible Layer and registers the returned address as the manager.
/// Because the value is defined by the contract's own code, deploying the contract is the
/// ownership proof: whoever controlled the deployment chose the manager. This is what makes
/// updated or redeployed contracts self-verifying, with no separate claim step.
///
/// Implementing this interface is optional. Contracts that do not expose it (for example,
/// already-deployed contracts that cannot be changed) go through manual verification, where
/// Phylax confirms ownership directly and sets the manager.
interface IInitialProtocolManager {
Comment thread
makemake-kbo marked this conversation as resolved.
/// @notice The address to set as this contract's protocol manager when the protocol is
/// initialized in the Credible Layer.
/// @return The intended initial protocol manager address.
function initialProtocolManager() external view returns (address);
}
37 changes: 37 additions & 0 deletions src/protection/credible_block/InitialProtocolManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IInitialProtocolManager} from "./IInitialProtocolManager.sol";

/// @title InitialProtocolManager
/// @author Phylax Systems
/// @notice Reusable base that implements {IInitialProtocolManager} by fixing the intended manager
/// at deployment. Inherit it and forward the manager address to the constructor; the
/// public immutable satisfies the interface's {initialProtocolManager} getter.
/// @dev The manager is immutable, so the value the state oracle reads is exactly what the deployer
/// committed to in the deployment transaction — that is the ownership proof. Changing the
/// declared manager after deployment means redeploying the inheriting contract. Once the
/// protocol is initialized in the Credible Layer, the manager is managed there rather than
/// through this value.
///
/// Example:
/// ```solidity
/// contract MyProtectedContract is InitialProtocolManager {
/// constructor(address manager) InitialProtocolManager(manager) {}
/// }
/// ```
abstract contract InitialProtocolManager is IInitialProtocolManager {
/// @inheritdoc IInitialProtocolManager
address public immutable initialProtocolManager;

/// @notice Thrown when constructed with the zero address as the initial protocol manager.
error ZeroInitialProtocolManager();

/// @param initialProtocolManager_ The address to declare as this contract's initial protocol
/// manager. Must be non-zero.
constructor(address initialProtocolManager_) {
if (initialProtocolManager_ == address(0)) revert ZeroInitialProtocolManager();

initialProtocolManager = initialProtocolManager_;
}
}
49 changes: 49 additions & 0 deletions src/protection/credible_block/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ guard; inherit `CredibleBlockGuard` directly when you want to protect your own f
- `ICredibleRegistry.sol` — read interface for the on-chain Credible Registry
(`isCredibleBlock(blockNumber)`, `lastCredibleBlock()`), mirroring `phylaxsystems/credible-registry`.
- `CredibleBlockGuard.sol` — abstract base contract providing the `onlyCredibleBlock` modifier.
- `IInitialProtocolManager.sol` — interface the state oracle reads to discover a contract's intended
protocol manager (`initialProtocolManager()`). See
[Initial protocol manager](#initial-protocol-manager).
- `InitialProtocolManager.sol` — abstract base implementing that interface with an immutable set at
deployment.

## Usage

Expand Down Expand Up @@ -62,8 +67,52 @@ approximating the chain's 15-minute budget:
Both the registry address and the threshold are immutable (configurable per deployment); re-pointing
or re-tuning means redeploying the inheriting contract.

## Initial protocol manager

Every protected contract needs a **protocol manager**: the address allowed to manage that
contract's assertions in the Credible Layer. Exposing the intended manager on the contract itself
lets the Credible Layer state oracle set it automatically, with no manual review round.

Because the address is defined by the contract's own code, **deploying the contract is the
ownership proof** — whoever controlled the deployment chose the manager. This is what makes updated
or redeployed contracts self-verifying: the state oracle calls `initialProtocolManager()` on the
new contract and registers the returned address, with no separate claim step.

Implementing this interface is **optional**. Contracts that don't expose it (for example,
already-deployed contracts you can't change) go through manual verification instead, where Phylax
confirms ownership directly and sets the manager.

Inherit the abstract base and forward the manager address to the constructor:

```solidity
import {InitialProtocolManager} from "credible-std/protection/credible_block/InitialProtocolManager.sol";

contract MyProtectedContract is InitialProtocolManager {
constructor(address manager) InitialProtocolManager(manager) {}
}
```

The public immutable auto-generates the `initialProtocolManager()` getter that satisfies the
interface. The manager is immutable, so the value the state oracle reads is exactly what the
deployer committed to; changing it means redeploying. Once the protocol is initialized in the
Credible Layer, the manager is managed there rather than through this value.

Contracts that only need to declare the manager (without the zero-address check or a base
constructor) can implement `IInitialProtocolManager` directly instead:

```solidity
import {IInitialProtocolManager} from "credible-std/protection/credible_block/IInitialProtocolManager.sol";

contract MyProtectedContract is IInitialProtocolManager {
function initialProtocolManager() external view returns (address) {
return 0xBEEF...;
}
}
```

## Tests

```sh
forge test --match-path "test/protection/credible_block/CredibleBlockGuard.t.sol"
forge test --match-path "test/protection/credible_block/InitialProtocolManager.t.sol"
```
14 changes: 12 additions & 2 deletions src/protection/safe/CredibleSafeGuard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.13;

import {ICredibleRegistry} from "./ICredibleRegistry.sol";
import {InitialProtocolManager} from "../credible_block/InitialProtocolManager.sol";

/// @notice Minimal subset of Safe's `Enum` library, vendored so the guard does not
/// depend on the Safe contracts package.
Expand Down Expand Up @@ -103,7 +104,12 @@ interface ITransactionGuard is IERC165 {
/// - ~1s blocks: 15 min ~= 900 blocks
/// Both the registry address and the fail-open threshold are immutable; re-pointing or
/// re-tuning means deploying a new guard and calling `setGuard` again.
contract CredibleSafeGuard is ITransactionGuard {
///
/// Onboarding. The guard also implements {IInitialProtocolManager} (via {InitialProtocolManager}),
/// exposing the address allowed to manage this deployment's assertions in the Credible Layer.
/// The state oracle reads it when the protocol is initialized, so deploying the guard is itself
/// the ownership proof — no separate manual review round.
contract CredibleSafeGuard is ITransactionGuard, InitialProtocolManager {
Comment thread
makemake-kbo marked this conversation as resolved.
/// @dev Bounds the gas and returndata exposure of each registry probe. A registry that cannot
/// answer within this budget is treated as unavailable and the guard fails open.
uint256 internal constant REGISTRY_READ_GAS_LIMIT = 50_000;
Expand All @@ -124,7 +130,11 @@ contract CredibleSafeGuard is ITransactionGuard {

/// @param credibleRegistry_ The Credible Registry address (configurable per deployment).
/// @param failOpenBlockThreshold_ Blocks of builder silence tolerated before failing open.
constructor(ICredibleRegistry credibleRegistry_, uint256 failOpenBlockThreshold_) {
/// @param initialProtocolManager_ Address allowed to manage this deployment's assertions in the
/// Credible Layer, exposed via {initialProtocolManager}. Must be non-zero.
constructor(ICredibleRegistry credibleRegistry_, uint256 failOpenBlockThreshold_, address initialProtocolManager_)
InitialProtocolManager(initialProtocolManager_)
{
if (address(credibleRegistry_) == address(0)) revert ZeroCredibleRegistryAddress();
if (failOpenBlockThreshold_ == 0) revert ZeroFailOpenBlockThreshold();

Expand Down
53 changes: 53 additions & 0 deletions test/protection/credible_block/InitialProtocolManager.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";

import {IInitialProtocolManager} from "../../../src/protection/credible_block/IInitialProtocolManager.sol";
import {InitialProtocolManager} from "../../../src/protection/credible_block/InitialProtocolManager.sol";

/// @notice Minimal concrete contract that inherits the abstract base by forwarding the manager
/// address to its constructor, mirroring the intended inherit-and-forward usage.
contract ProtectedContract is InitialProtocolManager {
constructor(address manager) InitialProtocolManager(manager) {}
}

contract InitialProtocolManagerTest is Test {
address internal constant MANAGER = address(0xA11CE);

ProtectedContract internal protectedContract;

function setUp() public {
protectedContract = new ProtectedContract(MANAGER);
}

// ---------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------

function test_constructor_storesManager() public view {
assertEq(protectedContract.initialProtocolManager(), MANAGER);
}

function test_constructor_revertsOnZeroManager() public {
vm.expectRevert(InitialProtocolManager.ZeroInitialProtocolManager.selector);
new ProtectedContract(address(0));
}

function testFuzz_constructor_storesAnyNonZeroManager(address manager) public {
vm.assume(manager != address(0));
ProtectedContract instance = new ProtectedContract(manager);
assertEq(instance.initialProtocolManager(), manager);
}

// ---------------------------------------------------------------------
// Interface conformance
// ---------------------------------------------------------------------

/// @dev The state oracle reads the manager through {IInitialProtocolManager}, so the public
/// immutable must satisfy that interface's getter when called through the interface type.
function test_conformsToInterface() public view {
IInitialProtocolManager asInterface = IInitialProtocolManager(address(protectedContract));
assertEq(asInterface.initialProtocolManager(), MANAGER);
}
}
Loading