Skip to content

Commit cc801d3

Browse files
authored
fix: claude-generated pre-audit of avm contract instance manager - completeness bug (#19254)
### Finding: Simulation Crash for Non-Existent Protocol Contracts **Type:** Completeness **Severity:** Medium (upgraded from Low after investigation) **Location:** `simulation/gadgets/contract_instance_manager.cpp:47` **Description:** In `ContractInstanceManager::get_contract_instance()`, when a protocol contract address (1 to MAX_PROTOCOL_CONTRACTS) is queried but the derived address is not found (nullopt), the code crashes: ```cpp if (ff_gt.ff_gt(MAX_PROTOCOL_CONTRACTS, contract_address - 1)) { std::optional<AztecAddress> derived_address = get_derived_address(protocol_contracts, contract_address); assert(derived_address.has_value() == maybe_instance.has_value() && ...); const ContractInstance& instance = maybe_instance.value(); // CRASH if nullopt event_emitter.emit({...}); return instance; } ``` **Evidence from Protocol:** Investigation of `yarn-project/protocol-contracts/src/protocol_contract_data.ts` reveals: - `MAX_PROTOCOL_CONTRACTS = 11` (reserved capacity) - Only **6 protocol contracts** are currently deployed (addresses 1-6) - **Addresses 7-11 are explicitly zero** (empty slots) ```typescript export const ProtocolContractsList = new ProtocolContracts([ // 1-6: Actual protocol contracts with derived addresses AztecAddress.fromString('0x01826...'), // AuthRegistry // ... 5 more ... // 7-11: Empty slots AztecAddress.fromString('0x0000000000...'), // EMPTY AztecAddress.fromString('0x0000000000...'), // EMPTY AztecAddress.fromString('0x0000000000...'), // EMPTY AztecAddress.fromString('0x0000000000...'), // EMPTY AztecAddress.fromString('0x0000000000...'), // EMPTY ]); ``` **Impact:** If a contract queries address 7, 8, 9, 10, or 11 using `GETCONTRACTINSTANCE`: 1. The simulation crashes instead of returning `exists = false` 2. Valid AVM programs that check non-existent protocol contract addresses will fail 3. The PIL constraints correctly handle this case (zero-check enforces `exists = 0`), but simulation prevents reaching that path **Recommendation:** Handle the case where `derived_address` is nullopt by emitting an event with `exists = false` and returning `std::nullopt`. **Status: FIXED** The fix was applied in `contract_instance_manager.cpp`: - Use `maybe_instance.value_or(ContractInstance{})` for the event (avoids `.value()` crash) - Use `derived_address.has_value()` for the `exists` field - Return `maybe_instance` directly (already an optional) - Added comments explaining protocol contract capacity vs. usage Test added: `ContractInstanceManagerTest.EmptyProtocolContractSlotReturnsNullopt`
1 parent e7d85ff commit cc801d3

File tree

2 files changed

+208
-7
lines changed

2 files changed

+208
-7
lines changed

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/contract_instance_manager.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,30 @@ std::optional<ContractInstance> ContractInstanceManager::get_contract_instance(c
3838
std::optional<ContractInstance> maybe_instance = contract_db.get_contract_instance(contract_address);
3939

4040
const auto& tree_state = merkle_db.get_tree_state();
41-
// If this is a canonical address
41+
42+
// Check if this is a protocol contract address (addresses 1 to MAX_PROTOCOL_CONTRACTS).
43+
// Protocol contracts are special reserved addresses that don't require nullifier checks.
4244
if (ff_gt.ff_gt(MAX_PROTOCOL_CONTRACTS, contract_address - 1)) {
43-
// Handle protocol contract addresses
45+
// Handle protocol contract addresses.
46+
// The derived_address lookup returns nullopt if this protocol contract slot is empty.
47+
// NOTE: MAX_PROTOCOL_CONTRACTS (currently 11) is the reserved capacity, but not all
48+
// slots may be filled. For example, addresses 1-6 are currently used while 7-11 are
49+
// empty (reserved for future protocol contracts).
4450
std::optional<AztecAddress> derived_address = get_derived_address(protocol_contracts, contract_address);
51+
52+
// Sanity check: if we found a derived address, we should also have the instance, and vice versa.
4553
assert(derived_address.has_value() == maybe_instance.has_value() &&
4654
"Derived address should be found if the instance was retrieved and vice versa");
47-
const ContractInstance& instance = maybe_instance.value();
55+
4856
event_emitter.emit({
4957
.address = contract_address,
50-
.contract_instance = maybe_instance.value_or<ContractInstance>({}),
51-
// Tree context
58+
.contract_instance = maybe_instance.value_or(ContractInstance{}),
5259
.nullifier_tree_root = tree_state.nullifier_tree.tree.root,
5360
.public_data_tree_root = tree_state.public_data_tree.tree.root,
5461
.exists = derived_address.has_value(),
5562
.is_protocol_contract = true,
5663
});
57-
58-
return instance;
64+
return maybe_instance;
5965
}
6066

6167
if (!merkle_db.nullifier_exists(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, contract_address)) {
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#include "barretenberg/vm2/simulation/gadgets/contract_instance_manager.hpp"
2+
3+
#include <gmock/gmock.h>
4+
#include <gtest/gtest.h>
5+
#include <optional>
6+
7+
#include "barretenberg/vm2/common/aztec_constants.hpp"
8+
#include "barretenberg/vm2/common/aztec_types.hpp"
9+
#include "barretenberg/vm2/simulation/events/contract_instance_retrieval_event.hpp"
10+
#include "barretenberg/vm2/simulation/events/event_emitter.hpp"
11+
#include "barretenberg/vm2/simulation/testing/mock_dbs.hpp"
12+
#include "barretenberg/vm2/simulation/testing/mock_field_gt.hpp"
13+
#include "barretenberg/vm2/simulation/testing/mock_update_check.hpp"
14+
#include "barretenberg/vm2/testing/fixtures.hpp"
15+
16+
using ::testing::_;
17+
using ::testing::Return;
18+
using ::testing::SizeIs;
19+
using ::testing::StrictMock;
20+
21+
namespace bb::avm2::simulation {
22+
namespace {
23+
24+
class ContractInstanceManagerTest : public ::testing::Test {
25+
protected:
26+
StrictMock<MockContractDB> contract_db;
27+
StrictMock<MockHighLevelMerkleDB> merkle_db;
28+
StrictMock<MockUpdateCheck> update_check;
29+
StrictMock<MockFieldGreaterThan> field_gt;
30+
EventEmitter<ContractInstanceRetrievalEvent> event_emitter;
31+
};
32+
33+
// Test that querying an empty protocol contract slot (addresses 7-11 are currently unused)
34+
// correctly returns nullopt with exists=false, rather than crashing.
35+
TEST_F(ContractInstanceManagerTest, EmptyProtocolContractSlotReturnsNullopt)
36+
{
37+
// Create protocol contracts with only first 6 slots filled (matching real configuration)
38+
// Slots 7-11 are zero (empty).
39+
ProtocolContracts protocol_contracts;
40+
for (uint32_t i = 0; i < 6; i++) {
41+
// Fill slots 0-5 (addresses 1-6) with non-zero derived addresses
42+
protocol_contracts.derived_addresses[i] = AztecAddress(FF(0x1000 + i));
43+
}
44+
// Slots 6-10 (addresses 7-11) are left as zero (default)
45+
46+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
47+
48+
// Query address 7 - an empty protocol contract slot
49+
AztecAddress empty_slot_address = AztecAddress(7);
50+
51+
// Setup mocks
52+
TreeStates tree_states = {};
53+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
54+
55+
// The contract DB should return nullopt since the instance doesn't exist
56+
EXPECT_CALL(contract_db, get_contract_instance(empty_slot_address)).WillOnce(Return(std::nullopt));
57+
58+
// ff_gt(MAX_PROTOCOL_CONTRACTS=11, 7-1=6) returns true because 11 > 6
59+
// This means address 7 IS in the protocol contract range
60+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), FF(6))).WillOnce(Return(true));
61+
62+
// The call should NOT crash and should return nullopt
63+
auto result = manager.get_contract_instance(empty_slot_address);
64+
65+
// Verify the result
66+
EXPECT_FALSE(result.has_value());
67+
68+
// Verify the event was emitted correctly
69+
auto events = event_emitter.dump_events();
70+
ASSERT_THAT(events, SizeIs(1));
71+
EXPECT_EQ(events[0].address, empty_slot_address);
72+
EXPECT_FALSE(events[0].exists);
73+
EXPECT_TRUE(events[0].is_protocol_contract);
74+
EXPECT_EQ(events[0].contract_instance, ContractInstance{});
75+
}
76+
77+
// Test that a valid protocol contract (e.g., address 1) works correctly
78+
TEST_F(ContractInstanceManagerTest, ValidProtocolContractReturnsInstance)
79+
{
80+
// Create protocol contracts with first slot filled
81+
ProtocolContracts protocol_contracts;
82+
AztecAddress derived_addr = AztecAddress(FF(0x12345));
83+
protocol_contracts.derived_addresses[0] = derived_addr; // Address 1 -> index 0
84+
85+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
86+
87+
// Query address 1 - a valid protocol contract
88+
AztecAddress protocol_address = AztecAddress(1);
89+
90+
// Create a contract instance
91+
ContractInstance instance = testing::random_contract_instance();
92+
93+
// Setup mocks
94+
TreeStates tree_states = {};
95+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
96+
EXPECT_CALL(contract_db, get_contract_instance(protocol_address)).WillOnce(Return(instance));
97+
98+
// ff_gt(MAX_PROTOCOL_CONTRACTS=11, 1-1=0) returns true because 11 > 0
99+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), FF(0))).WillOnce(Return(true));
100+
101+
// The call should succeed and return the instance
102+
auto result = manager.get_contract_instance(protocol_address);
103+
104+
// Verify the result
105+
ASSERT_TRUE(result.has_value());
106+
EXPECT_EQ(result.value(), instance);
107+
108+
// Verify the event was emitted correctly
109+
auto events = event_emitter.dump_events();
110+
ASSERT_THAT(events, SizeIs(1));
111+
EXPECT_EQ(events[0].address, protocol_address);
112+
EXPECT_TRUE(events[0].exists);
113+
EXPECT_TRUE(events[0].is_protocol_contract);
114+
EXPECT_EQ(events[0].contract_instance, instance);
115+
}
116+
117+
// Test that a regular (non-protocol) contract that exists works correctly
118+
TEST_F(ContractInstanceManagerTest, RegularContractExistsReturnsInstance)
119+
{
120+
ProtocolContracts protocol_contracts; // Empty - no protocol contracts
121+
122+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
123+
124+
// Query a regular address (outside protocol contract range)
125+
AztecAddress regular_address = AztecAddress(FF(0x1234567890ULL));
126+
127+
// Create a contract instance
128+
ContractInstance instance = testing::random_contract_instance();
129+
130+
// Setup mocks
131+
TreeStates tree_states = {};
132+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
133+
EXPECT_CALL(contract_db, get_contract_instance(regular_address)).WillOnce(Return(instance));
134+
135+
// ff_gt(11, large_address - 1) returns false - not in protocol range
136+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), regular_address - 1)).WillOnce(Return(false));
137+
138+
// Nullifier exists - contract is deployed
139+
EXPECT_CALL(merkle_db, nullifier_exists(FF(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS), regular_address))
140+
.WillOnce(Return(true));
141+
142+
// Update check is performed for regular contracts
143+
EXPECT_CALL(update_check, check_current_class_id(regular_address, instance));
144+
145+
// The call should succeed
146+
auto result = manager.get_contract_instance(regular_address);
147+
148+
// Verify the result
149+
ASSERT_TRUE(result.has_value());
150+
EXPECT_EQ(result.value(), instance);
151+
152+
// Verify the event
153+
auto events = event_emitter.dump_events();
154+
ASSERT_THAT(events, SizeIs(1));
155+
EXPECT_EQ(events[0].address, regular_address);
156+
EXPECT_TRUE(events[0].exists);
157+
EXPECT_FALSE(events[0].is_protocol_contract);
158+
}
159+
160+
// Test that a regular contract that doesn't exist returns nullopt
161+
TEST_F(ContractInstanceManagerTest, RegularContractNotExistsReturnsNullopt)
162+
{
163+
ProtocolContracts protocol_contracts; // Empty
164+
165+
ContractInstanceManager manager(contract_db, merkle_db, update_check, field_gt, protocol_contracts, event_emitter);
166+
167+
AztecAddress non_existent_address = AztecAddress(FF(0xDEADBEEFULL));
168+
169+
// Setup mocks
170+
TreeStates tree_states = {};
171+
EXPECT_CALL(merkle_db, get_tree_state()).WillOnce(Return(tree_states));
172+
EXPECT_CALL(contract_db, get_contract_instance(non_existent_address)).WillOnce(Return(std::nullopt));
173+
174+
// Not in protocol range
175+
EXPECT_CALL(field_gt, ff_gt(FF(MAX_PROTOCOL_CONTRACTS), non_existent_address - 1)).WillOnce(Return(false));
176+
177+
// Nullifier doesn't exist - contract not deployed
178+
EXPECT_CALL(merkle_db, nullifier_exists(FF(CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS), non_existent_address))
179+
.WillOnce(Return(false));
180+
181+
// The call should return nullopt
182+
auto result = manager.get_contract_instance(non_existent_address);
183+
184+
EXPECT_FALSE(result.has_value());
185+
186+
// Verify the event
187+
auto events = event_emitter.dump_events();
188+
ASSERT_THAT(events, SizeIs(1));
189+
EXPECT_EQ(events[0].address, non_existent_address);
190+
EXPECT_FALSE(events[0].exists);
191+
EXPECT_FALSE(events[0].is_protocol_contract);
192+
}
193+
194+
} // namespace
195+
} // namespace bb::avm2::simulation

0 commit comments

Comments
 (0)