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
16 changes: 0 additions & 16 deletions .github/workflows/solidity-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,3 @@ jobs:
exit 1
fi
echo "Verified ${assertion_count} assertion-running test case(s) across ${testcase_count} tests"

# Real Gnosis Safe compatibility and deployment tooling use the legacy compiler pipeline and
# plain Forge intentionally; they do not depend on Credible assertion cheatcodes.
safe-guard-integration:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Run Safe guard compatibility and script tests
run: FOUNDRY_PROFILE=safe-guard forge test
5 changes: 0 additions & 5 deletions examples/balancer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

Assertion examples for Balancer V3's singleton Vault architecture.

> **Unavailable:** `BalancerV3VaultAssertion` is quarantined and registers no triggers. Hook and
> token-rate behavior must be derived from live registered Vault configuration and bound to a
> supported pool/factory revision before this wrapper can be activated. The outflow circuit breaker
> remains a separate operational policy.

## Build

```sh
Expand Down
17 changes: 15 additions & 2 deletions examples/balancer/src/BalancerV3VaultAssertion.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,21 @@ contract BalancerV3VaultAssertion is BalancerV3VaultHelpers {
/// on the watched pool's own accounting deltas and the custody check compares per-token
/// deficits across the transaction — both bounded by the pool's token count.
function triggers() external view override {
// Quarantined: hook classification must be read from the Vault and the supported rate
// policy must be derived from the registered token configuration, not deployment flags.
registerFnCallTrigger(this.assertSwapPreservesPoolInvariant.selector, IBalancerV3VaultLike.swap.selector);
registerFnCallTrigger(
this.assertOperationRatesWithinBaseline.selector, IBalancerV3VaultLike.addLiquidity.selector
);
registerFnCallTrigger(
this.assertOperationRatesWithinBaseline.selector, IBalancerV3VaultLike.removeLiquidity.selector
);
registerFnCallTrigger(
this.assertOperationRatesWithinBaseline.selector, IBalancerV3VaultLike.initialize.selector
);
registerFnCallTrigger(
this.assertOperationRatesWithinBaseline.selector, IBalancerV3VaultLike.disableRecoveryMode.selector
);
registerTxEndTrigger(this.assertPoolAccountingWithinVaultCustody.selector);
registerTxEndTrigger(this.assertTokenRatesWithinDriftBound.selector);
}

/// @notice A swap on a hookless pool must grow (or at worst preserve) the pool invariant,
Expand Down
72 changes: 30 additions & 42 deletions examples/balancer/test/BalancerV3VaultAssertion.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,6 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
cl.assertion(address(vault), createData, fnSelector);
}

function testPublishedWrapperCannotRegisterUnsafeChecks() public {
BalancerV3VaultAssertion assertion = new BalancerV3VaultAssertion(
address(vault), address(pool), vault.swapHooks(), INVARIANT_DUST_TOLERANCE, RATE_DRIFT_TOLERANCE_BPS
);
vm.mockCallRevert(
address(uint160(uint256(keccak256("TriggerRecorder")))),
bytes(""),
bytes("quarantined wrapper registered a trigger")
);
assertion.triggers();
}

function _swapParams(address targetPool) internal view returns (VaultSwapParams memory) {
return VaultSwapParams({
kind: SwapKind.EXACT_IN,
Expand Down Expand Up @@ -104,45 +92,45 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {

// --- assertSwapPreservesPoolInvariant ------------------------------------

function retiredHonestSwapPassesInvariantAssertion() public {
function testHonestSwapPassesInvariantAssertion() public {
_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
_swap(address(pool));
}

/// @notice The Vault deducts pending yield fees before adding swap input. Raw tokenIn balance
/// can therefore fall across an honest small swap; fee-adjusted live balance still
/// moves in the correct direction and is the relevant input to pool math.
function retiredHonestSwapWithPendingYieldFeesPassesInvariantAssertion() public {
function testHonestSwapWithPendingYieldFeesPassesInvariantAssertion() public {
vault.seedPendingYieldFee(address(token0), 20e18);

_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
_swap(address(pool));
}

function retiredSwapOnUnwatchedPoolIsIgnored() public {
function testSwapOnUnwatchedPoolIsIgnored() public {
vault.setMode(MockBalancerV3Vault.Mode.InvariantLoss);

_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
_swap(makeAddr("otherPool"));
}

function retiredInvariantLossTrips() public {
function testInvariantLossTrips() public {
vault.setMode(MockBalancerV3Vault.Mode.InvariantLoss);

_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
vm.expectRevert(bytes("BalancerV3: swap decreased pool invariant"));
_swap(address(pool));
}

function retiredSupplyDriftTrips() public {
function testSupplyDriftTrips() public {
vault.setMode(MockBalancerV3Vault.Mode.SupplyDrift);

_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
vm.expectRevert(bytes("BalancerV3: swap changed BPT supply"));
_swap(address(pool));
}

function retiredBalancesAgainstSwapDirectionTrip() public {
function testBalancesAgainstSwapDirectionTrip() public {
vault.setMode(MockBalancerV3Vault.Mode.BalanceSwapEnds);

_arm(BalancerV3VaultAssertion.assertSwapPreservesPoolInvariant.selector);
Expand All @@ -154,7 +142,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// the swap check's scope: those hooks may legitimately reenter the Vault mid-swap, so
/// call-boundary snapshots cannot attribute deltas to the core swap. A failure knob
/// that would otherwise trip must pass once the deployment marks the pool as hooked.
function retiredHookedPoolSwapChecksAreSkipped() public {
function testHookedPoolSwapChecksAreSkipped() public {
vault.setSwapHooks(true);
vault.setMode(MockBalancerV3Vault.Mode.InvariantLoss);

Expand All @@ -166,7 +154,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// restored after it, inside one transaction. Both transaction endpoints agree, but
/// the swap priced against the shifted rate — the per-operation baseline observation
/// catches what endpoint comparison cannot.
function retiredTransientRateManipulationAroundSwapTrips() public {
function testTransientRateManipulationAroundSwapTrips() public {
RateManipulatingRouter router = new RateManipulatingRouter(vault, rateProvider);
token0.mint(address(router), 100e18);
router.approveVault(address(token0));
Expand All @@ -179,7 +167,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// @notice Documents the endpoint-comparison gap the per-operation check exists for: the same
/// manipulate-swap-restore transaction passes the tx-end drift assertion because the
/// pre-tx and post-tx rates are equal.
function retiredTransientRateManipulationPassesEndpointDrift() public {
function testTransientRateManipulationPassesEndpointDrift() public {
RateManipulatingRouter router = new RateManipulatingRouter(vault, rateProvider);
token0.mint(address(router), 100e18);
router.approveVault(address(token0));
Expand All @@ -188,20 +176,20 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
router.manipulateSwapRestore(_swapParams(address(pool)));
}

function retiredTransientRateManipulationAroundAddLiquidityTrips() public {
function testTransientRateManipulationAroundAddLiquidityTrips() public {
RateManipulatingRouter router = new RateManipulatingRouter(vault, rateProvider);

_arm(BalancerV3VaultAssertion.assertOperationRatesWithinBaseline.selector);
vm.expectRevert(bytes("BalancerV3: liquidity priced against rate beyond drift bound"));
router.manipulateAddLiquidityRestore(_addLiquidityParams(address(pool)));
}

function retiredHonestAddLiquidityPassesScopedRateAssertion() public {
function testHonestAddLiquidityPassesScopedRateAssertion() public {
_arm(BalancerV3VaultAssertion.assertOperationRatesWithinBaseline.selector);
vault.addLiquidity(_addLiquidityParams(address(pool)));
}

function retiredTransientRateManipulationAroundRemoveLiquidityTrips() public {
function testTransientRateManipulationAroundRemoveLiquidityTrips() public {
RateManipulatingRouter router = new RateManipulatingRouter(vault, rateProvider);

_arm(BalancerV3VaultAssertion.assertOperationRatesWithinBaseline.selector);
Expand All @@ -211,20 +199,20 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {

// --- assertPoolAccountingWithinVaultCustody -------------------------------

function retiredHonestSwapPassesCustodyAssertion() public {
function testHonestSwapPassesCustodyAssertion() public {
_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
_swap(address(pool));
}

function retiredReserveSkimTripsCustody() public {
function testReserveSkimTripsCustody() public {
vault.setMode(MockBalancerV3Vault.Mode.ReserveSkim);

_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
vm.expectRevert(bytes("BalancerV3: vault reserves exceed real token custody"));
_swap(address(pool));
}

function retiredPhantomPoolBalanceTripsCustody() public {
function testPhantomPoolBalanceTripsCustody() public {
vault.setMode(MockBalancerV3Vault.Mode.PhantomBalance);

_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
Expand All @@ -236,15 +224,15 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// stay untouched: no watched-pool accounting delta exists, yet the deficit comparison
/// still blocks the causing transaction instead of deferring detection to the pool's
/// next accounting-moving transaction.
function retiredCustodyDrainWithoutAccountingChangeTrips() public {
function testCustodyDrainWithoutAccountingChangeTrips() public {
_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
vm.expectRevert(bytes("BalancerV3: vault reserves exceed real token custody"));
vault.sweepCustodyOnly(address(token0), 10e18);
}

/// @notice A deficit that predates the transaction is not a license: a transaction that
/// deepens it still trips, even though the absolute bound was already violated before.
function retiredWorseningPreexistingDeficitTrips() public {
function testWorseningPreexistingDeficitTrips() public {
vault.seedPoolBalance(1, 1_600e18); // claim already exceeds reserves before arming
vault.setMode(MockBalancerV3Vault.Mode.PhantomBalance);

Expand All @@ -255,7 +243,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {

/// @notice A pool already in deficit must remain usable by honest traffic: an operation that
/// reduces the pool's claims (still violated in absolute terms, but no worse) passes.
function retiredHonestOperationOnDeficientPoolPasses() public {
function testHonestOperationOnDeficientPoolPasses() public {
vault.seedPoolBalance(0, 2_000e18); // claim exceeds reserves before arming

_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
Expand All @@ -264,12 +252,12 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {

// --- assertTokenRatesWithinDriftBound --------------------------------------

function retiredHonestSwapPassesRateAssertion() public {
function testHonestSwapPassesRateAssertion() public {
_arm(BalancerV3VaultAssertion.assertTokenRatesWithinDriftBound.selector);
_swap(address(pool));
}

function retiredRateShiftTrips() public {
function testRateShiftTrips() public {
vault.setMode(MockBalancerV3Vault.Mode.RateShift);

_arm(BalancerV3VaultAssertion.assertTokenRatesWithinDriftBound.selector);
Expand All @@ -280,7 +268,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// @notice A provider already registered for the pool pre-tx that answered ZERO pre-tx is a
/// broken baseline, not a deployment lifecycle: it must fail instead of granting the
/// registration exemption and legitimizing an arbitrary post-tx rate.
function retiredZeroBaselineForRegisteredProviderTrips() public {
function testZeroBaselineForRegisteredProviderTrips() public {
rateProvider.setRate(0);
vault.setMode(MockBalancerV3Vault.Mode.RateShift); // 0 -> 1e18 during the swap

Expand All @@ -292,14 +280,14 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// @notice A provider deployed and registered within the transaction has no pre-tx baseline by
/// construction: the deployment lifecycle is exempt from the drift comparison (only the
/// nonzero post-state is enforced) instead of reverting on the missing baseline read.
function retiredProviderRegisteredDuringTxIsExempt() public {
function testProviderRegisteredDuringTxIsExempt() public {
vault.registerNewRateProvider();

_arm(BalancerV3VaultAssertion.assertTokenRatesWithinDriftBound.selector);
vault.initialize(address(pool), address(this), new address[](0), new uint256[](0), 0, "");
}

function retiredProviderRegisteredDuringTxPassesScopedInitializationRateCheck() public {
function testProviderRegisteredDuringTxPassesScopedInitializationRateCheck() public {
vault.registerNewRateProvider();

_arm(BalancerV3VaultAssertion.assertOperationRatesWithinBaseline.selector);
Expand All @@ -309,7 +297,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// @notice Recovery mode disables the rate assertion entirely: Balancer's recovery exit uses
/// raw balances precisely because providers may be broken, and a broken or moved
/// provider must never block that path.
function retiredRecoveryModeSkipsRateAssertion() public {
function testRecoveryModeSkipsRateAssertion() public {
vault.setRecoveryMode(true);
vault.setMode(MockBalancerV3Vault.Mode.RateShift);

Expand All @@ -321,7 +309,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// out of the drift assertion's scope, so the watched provider never becomes a
/// dependency of unrelated Vault traffic. The residual is documented on the assertion:
/// a later transaction consuming the moved rate touches the pool and is examined.
function retiredRateOnlyTransactionIsOutOfScope() public {
function testRateOnlyTransactionIsOutOfScope() public {
_arm(BalancerV3VaultAssertion.assertTokenRatesWithinDriftBound.selector);
vault.shiftRateOnly();
}
Expand All @@ -330,7 +318,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// unrelated singleton traffic is skipped without consulting the rate provider even
/// when that provider is broken: the watched provider never becomes a liveness
/// dependency of the rest of the singleton.
function retiredUnrelatedVaultTrafficIgnoresBrokenProvider() public {
function testUnrelatedVaultTrafficIgnoresBrokenProvider() public {
rateProvider.setRate(0); // would trip "returned zero rate" if the drift loop ran

_arm(BalancerV3VaultAssertion.assertTokenRatesWithinDriftBound.selector);
Expand All @@ -340,7 +328,7 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {
/// @notice A custody imbalance that predates the transaction is flagged at the transaction
/// that caused it, not re-litigated by every later unrelated transaction: the deficit
/// comparison sees an unchanged baseline and lets the transaction through.
function retiredUnrelatedVaultTrafficSkipsPreexistingCustodyImbalance() public {
function testUnrelatedVaultTrafficSkipsPreexistingCustodyImbalance() public {
vault.seedPoolBalance(0, 2_000e18); // pool claims exceed reserves before the armed tx

_arm(BalancerV3VaultAssertion.assertPoolAccountingWithinVaultCustody.selector);
Expand All @@ -349,19 +337,19 @@ contract BalancerV3VaultAssertionTest is Test, CredibleTest {

// --- wiring ----------------------------------------------------------------

function retiredDeploys() public {
function testDeploys() public {
BalancerV3VaultAssertion assertion = new BalancerV3VaultAssertion(
address(vault), address(pool), false, INVARIANT_DUST_TOLERANCE, RATE_DRIFT_TOLERANCE_BPS
);
assertTrue(address(assertion) != address(0));
}

function retiredRejectsZeroVault() public {
function testRejectsZeroVault() public {
vm.expectRevert(bytes("BalancerV3: zero vault"));
new BalancerV3VaultAssertion(address(0), address(pool), false, 0, RATE_DRIFT_TOLERANCE_BPS);
}

function retiredRejectsZeroPool() public {
function testRejectsZeroPool() public {
vm.expectRevert(bytes("BalancerV3: zero pool"));
new BalancerV3VaultAssertion(address(vault), address(0), false, 0, RATE_DRIFT_TOLERANCE_BPS);
}
Expand Down