Skip to content

Commit 830a023

Browse files
authored
Merge pull request #92 from PracticalParticle/work
Work
2 parents 53988ab + 727fc1c commit 830a023

9 files changed

Lines changed: 403 additions & 63 deletions

contracts/core/lib/EngineBlox.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,10 @@ library EngineBlox {
21162116
bytes4 handlerSelector,
21172117
TxAction action
21182118
) internal view {
2119+
// Ensure both execution and handler selectors have registered function schemas
2120+
_validateFunctionSchemaExists(self, executionSelector);
2121+
_validateFunctionSchemaExists(self, handlerSelector);
2122+
21192123
// Validate permission for the execution selector (underlying operation)
21202124
if (!hasActionPermission(self, wallet, executionSelector, action)) {
21212125
revert SharedValidation.NoPermission(wallet);

sdk/typescript/docs/runtime-rbac.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ Roles have maximum wallet limits:
299299

300300
```typescript
301301
const role = await runtimeRBAC.getRole(roleHash)
302-
const wallets = await runtimeRBAC.getWalletsInRole(roleHash)
302+
const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)
303303

304304
if (wallets.length >= role.maxWallets) {
305305
throw new Error('Role has reached maximum wallet limit')
@@ -435,7 +435,7 @@ describe('RuntimeRBAC', () => {
435435
})
436436

437437
it('should get wallets in role', async () => {
438-
const wallets = await runtimeRBAC.getWalletsInRole(roleHash)
438+
const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)
439439
expect(Array.isArray(wallets)).toBe(true)
440440
})
441441
})

test/foundry/docs/ATTACK_VECTORS_CODEX.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,43 @@ removeFunctionSchema(selector, false);
20862086

20872087
---
20882088

2089+
#### MEDIUM: Unsafe Function Unregistration with Stale Permissions
2090+
- **ID**: `FS-004`
2091+
- **Location**: `EngineBlox.sol:1185-1239`, `EngineBlox.sol:2104-2129`
2092+
- **Severity**: MEDIUM
2093+
- **Status**: ✅ **PROTECTED**
2094+
2095+
**Description**:
2096+
Using `unregisterFunction(functionSelector, false)` to remove a function schema while roles still hold permissions for the selector, then attempting to create or execute new transactions using the stale selector.
2097+
2098+
**Attack Scenario**:
2099+
```solidity
2100+
// 1. Register function schema and grant role permission for EXECUTE_TIME_DELAY_REQUEST
2101+
registerFunction(schemaSelector, ...);
2102+
addFunctionToRole(ROLE, FunctionPermission({ functionSelector: schemaSelector, ... }));
2103+
2104+
// 2. Unsafely unregister schema while role still references selector
2105+
unregisterFunction(schemaSelector, false); // safeRemoval = false
2106+
2107+
// 3. Attempt to create or approve new transaction using stale selector
2108+
txRequest(..., handlerSelector = schemaSelector, executionSelector = schemaSelector, ...); // Should fail
2109+
```
2110+
2111+
**Current Protection**:
2112+
-`_validateFunctionSchemaExists` ensures a function schema exists for any selector being executed
2113+
-`_validateExecutionAndHandlerPermissions` enforces schema existence for both `executionSelector` and `handlerSelector`
2114+
- ✅ Requests and approvals using unregistered selectors revert with `ResourceNotFound`
2115+
2116+
**Verification**:
2117+
- Test unsafe unregistration followed by `txRequest` and approval flows
2118+
- Verify that requests and approvals revert when function schema has been unregistered
2119+
- Confirm that `safeRemoval = true` continues to enforce "no stale role permissions" invariant
2120+
2121+
**Related Tests**:
2122+
- `UnregisterFunctionFuzz.t.sol::testFuzz_UnsafeUnregisterPreventsNewRequests`
2123+
2124+
---
2125+
20892126
## 12. Initialization & Upgrade
20902127

20912128
### 12.1 Initialization Attacks

test/foundry/docs/FINAL_COVERAGE_REPORT.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Final Fuzz Test Coverage Report
22

3-
**Date**: February 21, 2026
3+
**Date**: March 3, 2026
44
**Status**: ✅ **COMPREHENSIVE TEST SUITE COMPLETE**
55
**Goal**: 100% Coverage of All Attack Vectors
66

77
---
88

99
## Summary
1010

11-
A comprehensive fuzz test suite covers **all 207+ attack vectors** identified in the security analysis, plus **21 protocol-vulnerabilities-index-derived vectors** (see [Attack Vectors Codex §18](./ATTACK_VECTORS_CODEX.md#18-protocol-vulnerabilities-index-derived-vectors)). The suite consists of **14 comprehensive fuzz files** with **148 tests**, plus additional fuzz, invariant, and unit tests: **37 test suites, 309 tests** (all passing as of last full run).
11+
A comprehensive fuzz test suite covers **all 207+ attack vectors** identified in the security analysis, plus **21 protocol-vulnerabilities-index-derived vectors** (see [Attack Vectors Codex §18](./ATTACK_VECTORS_CODEX.md#18-protocol-vulnerabilities-index-derived-vectors)). The suite consists of **14 comprehensive fuzz files** with **149 tests**, plus additional fuzz, invariant, and unit tests: **38 test suites, 308 tests** (all passing as of last full run, including `UnregisterFunctionFuzz.t.sol`).
1212

1313
---
1414

@@ -97,8 +97,8 @@ A comprehensive fuzz test suite covers **all 207+ attack vectors** identified in
9797
- ✅ Malicious forwarder isolation
9898
- ✅ Gas exhaustion handling
9999

100-
### Target Whitelist & Function Schema (6 vectors)
101-
-**100% coverage** - 8/8 tests passing
100+
### Target Whitelist & Function Schema (7 vectors)
101+
-**100% coverage** - 9/9 tests passing
102102
- ✅ Empty whitelist denial
103103
- ✅ Whitelist removal prevention
104104
- ✅ Handler selector validation
@@ -154,22 +154,22 @@ A comprehensive fuzz test suite covers **all 207+ attack vectors** identified in
154154
| Initialization | 3 | 9 | 100% |
155155
| Hook System | 4 | 2 | 100% |
156156
| Event Forwarding | 2 | 2 | 100% |
157-
| Whitelist/Schema | 6 | 8 | 100% |
157+
| Whitelist/Schema | 7 | 9 | 100% |
158158
| Time-Based | 3 | 1* | 100%* |
159159
| Role Management | 3 | 3* | 100%* |
160160
| Security Edge Cases | 10 | 10 | 100% |
161161
| Gas Exhaustion | 17 | 17 | 100% |
162162
| Protocol-Vulnerabilities-Index (§18) | 21 | 17 covered, 4 N/A | 100%* |
163-
| **TOTAL** | **207+** | **309 tests (37 suites)** | **100%** |
163+
| **TOTAL** | **207+** | **308 tests (38 suites)** | **100%** |
164164

165165
*Covered in other test files. §18: 17 vectors covered by comprehensive suite; 4 N/A (no delegatecall/approve-before-call/proxy pattern).
166166

167167
---
168168

169169
## Test Execution Results
170170

171-
### Current Status (February 2026)
172-
-**37 test suites**, **309 tests** (all passing; includes 14 comprehensive fuzz files with 148 tests)
171+
### Current Status (March 2026)
172+
-**38 test suites**, **308 tests** (all passing; includes 14 comprehensive fuzz files with 149 tests and additional targeted fuzz like `UnregisterFunctionFuzz.t.sol`)
173173
-**All critical attack vectors** covered
174174
-**All high-priority attack vectors** covered
175175
-**Protocol-vulnerabilities-index-derived vectors**: 17 covered/partial, 4 N/A — see [Codex §18](./ATTACK_VECTORS_CODEX.md#18-protocol-vulnerabilities-index-derived-vectors)
@@ -202,7 +202,7 @@ A comprehensive fuzz test suite covers **all 207+ attack vectors** identified in
202202
### ✅ Complete Test Coverage Structure
203203

204204
1. **14 comprehensive test files** organized by attack category
205-
2. **309 test functions** covering all attack vectors
205+
2. **308 test functions** covering all attack vectors
206206
3. **Direct mapping** to security analysis documents
207207
4. **Clear documentation** with execution guides
208208
5. **System safety limits** verified and tested

test/foundry/docs/TEST_DOCUMENTATION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,12 @@ This document provides comprehensive documentation of all test functions in the
10281028
- **Purpose**: Fuzz tests for state machine workflows
10291029
- **Status**: ✅ Tests passing
10301030

1031+
#### UnregisterFunctionFuzz.t.sol
1032+
- **Location**: `test/foundry/fuzz/UnregisterFunctionFuzz.t.sol`
1033+
- **Purpose**: Fuzz tests for unsafe function schema unregistration (`unregisterFunction` with `safeRemoval = false`) and stale role permissions
1034+
- **Covers**: [FS-004](./ATTACK_VECTORS_CODEX.md#medium-unsafe-function-unregistration-with-stale-permissions) – ensures requests using unregistered selectors revert via schema validation
1035+
- **Status**: ✅ Tests passing
1036+
10311037
#### ProtectedResourceFuzz.t.sol
10321038
- **Location**: `test/foundry/fuzz/ProtectedResourceFuzz.t.sol`
10331039
- **Purpose**: Fuzz tests for protected resources

test/foundry/docs/TEST_EXECUTION_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
This guide provides comprehensive instructions for executing the Bloxchain Protocol test suite, including comprehensive fuzz tests, unit tests, integration tests, and security tests.
1212

13-
**Test Suite Status**: ✅ **All tests passing** (37 test suites, 309 tests passed)
13+
**Test Suite Status**: ✅ **All tests passing** (38 test suites, 308 tests passed)
1414

1515
---
1616

@@ -41,7 +41,7 @@ forge test --match-test "testFuzz_BatchOperationAtomicity" -vv
4141
## Test Suite Structure
4242

4343
### Full suite summary
44-
- **37 test suites**, **309 tests** (all passing). Includes fuzz, unit, integration, and security tests.
44+
- **38 test suites**, **308 tests** (all passing). Includes fuzz, unit, integration, and security tests.
4545

4646
### Comprehensive Fuzz Tests (10 files – subset of full suite)
4747

test/foundry/fuzz/ComprehensiveCompositeFuzz.t.sol

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ pragma solidity 0.8.34;
44
import "../CommonBase.sol";
55
import "../../../contracts/core/access/RuntimeRBAC.sol";
66
import "../../../contracts/core/execution/GuardController.sol";
7+
import "../../../contracts/core/execution/interface/IGuardController.sol";
8+
import "../../../contracts/core/execution/lib/definitions/GuardControllerDefinitions.sol";
79
import "../../../contracts/core/access/lib/definitions/RuntimeRBACDefinitions.sol";
810
import "../../../contracts/core/lib/utils/SharedValidation.sol";
911
import "../helpers/TestHelpers.sol";
@@ -32,8 +34,17 @@ contract ComprehensiveCompositeFuzzTest is CommonBase {
3234

3335
function setUp() public override {
3436
super.setUp();
35-
// Ensure mockTarget is whitelisted for executeWithTimeLock tests
36-
// Note: This should be done in CommonBase, but we ensure it here for Composite tests
37+
38+
// Register function schema for execute() on mockTarget and grant owner REQUEST+APPROVE
39+
bytes4 executeSelector = bytes4(keccak256("execute()"));
40+
EngineBlox.TxAction[] memory actions = new EngineBlox.TxAction[](2);
41+
actions[0] = EngineBlox.TxAction.EXECUTE_TIME_DELAY_REQUEST;
42+
actions[1] = EngineBlox.TxAction.EXECUTE_TIME_DELAY_APPROVE;
43+
_registerFunction("execute()", "TEST_OPERATION", actions);
44+
_grantOwnerPermission(executeSelector, actions);
45+
46+
// Whitelist mockTarget for executeWithTimeLock tests
47+
_whitelistTarget(address(mockTarget), executeSelector);
3748
}
3849

3950
/// @dev Converts uint to decimal string for deterministic role names (avoids vm.assume reject limit).
@@ -335,11 +346,19 @@ contract ComprehensiveCompositeFuzzTest is CommonBase {
335346
// Attempt to execute before time-lock expires
336347
if (block.timestamp < releaseTime) {
337348
vm.prank(broadcaster);
338-
accountBlox.approveTimeLockExecutionWithMetaTx(metaTx);
339-
340-
// Should fail - time-lock not expired
341-
// Note: Meta-transaction approval still checks releaseTime
342-
// This verifies time-lock applies to meta-transactions
349+
try accountBlox.approveTimeLockExecutionWithMetaTx(metaTx) {
350+
// If this ever succeeds before releaseTime, the underlying implementation is broken.
351+
// Time-lock bypass via meta-transaction is a critical regression: fail the test immediately.
352+
fail("approveTimeLockExecutionWithMetaTx succeeded before releaseTime");
353+
} catch (bytes memory reason) {
354+
bytes4 sel = bytes4(reason);
355+
if (sel == SharedValidation.NoPermission.selector) {
356+
return; // Security guard: this path simply isn't authorized in this fuzz run
357+
}
358+
assembly {
359+
revert(add(reason, 0x20), mload(reason))
360+
}
361+
}
343362
}
344363
} catch (bytes memory reason) {
345364
bytes4 errorSelector = bytes4(reason);
@@ -570,4 +589,155 @@ contract ComprehensiveCompositeFuzzTest is CommonBase {
570589
}
571590
revert("No matching private key found");
572591
}
592+
593+
/**
594+
* @dev Helper to register a function schema on accountBlox via GuardController guardConfigBatch
595+
*/
596+
function _registerFunction(
597+
string memory functionSignature,
598+
string memory operationName,
599+
EngineBlox.TxAction[] memory supportedActions
600+
) internal {
601+
require(supportedActions.length > 0, "Supported actions cannot be empty");
602+
603+
IGuardController.GuardConfigAction[] memory actions = new IGuardController.GuardConfigAction[](1);
604+
actions[0] = IGuardController.GuardConfigAction({
605+
actionType: IGuardController.GuardConfigActionType.REGISTER_FUNCTION,
606+
data: abi.encode(functionSignature, operationName, supportedActions)
607+
});
608+
609+
bytes memory params = GuardControllerDefinitions.guardConfigBatchExecutionParams(actions);
610+
611+
EngineBlox.MetaTxParams memory metaTxParams = accountBlox.createMetaTxParams(
612+
address(accountBlox),
613+
GuardControllerDefinitions.GUARD_CONFIG_BATCH_META_SELECTOR,
614+
EngineBlox.TxAction.SIGN_META_REQUEST_AND_APPROVE,
615+
1 hours,
616+
0,
617+
owner
618+
);
619+
620+
EngineBlox.MetaTransaction memory metaTx = accountBlox.generateUnsignedMetaTransactionForNew(
621+
owner,
622+
address(accountBlox),
623+
0,
624+
0,
625+
GuardControllerDefinitions.CONTROLLER_OPERATION,
626+
GuardControllerDefinitions.GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
627+
params,
628+
metaTxParams
629+
);
630+
631+
uint256 signerPrivateKey = _getPrivateKeyForAddress(owner);
632+
bytes32 messageHash = metaTx.message;
633+
bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
634+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, ethSignedMessageHash);
635+
metaTx.signature = abi.encodePacked(r, s, v);
636+
637+
vm.prank(broadcaster);
638+
accountBlox.guardConfigBatchRequestAndApprove(metaTx);
639+
}
640+
641+
/**
642+
* @dev Helper to whitelist a target for a function selector on accountBlox
643+
*/
644+
function _whitelistTarget(address target, bytes4 selector) internal {
645+
IGuardController.GuardConfigAction[] memory actions = new IGuardController.GuardConfigAction[](1);
646+
actions[0] = IGuardController.GuardConfigAction({
647+
actionType: IGuardController.GuardConfigActionType.ADD_TARGET_TO_WHITELIST,
648+
data: abi.encode(selector, target)
649+
});
650+
651+
bytes memory params = GuardControllerDefinitions.guardConfigBatchExecutionParams(actions);
652+
653+
EngineBlox.MetaTxParams memory metaTxParams = accountBlox.createMetaTxParams(
654+
address(accountBlox),
655+
GuardControllerDefinitions.GUARD_CONFIG_BATCH_META_SELECTOR,
656+
EngineBlox.TxAction.SIGN_META_REQUEST_AND_APPROVE,
657+
1 hours,
658+
0,
659+
owner
660+
);
661+
662+
EngineBlox.MetaTransaction memory metaTx = accountBlox.generateUnsignedMetaTransactionForNew(
663+
owner,
664+
address(accountBlox),
665+
0,
666+
0,
667+
GuardControllerDefinitions.CONTROLLER_OPERATION,
668+
GuardControllerDefinitions.GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
669+
params,
670+
metaTxParams
671+
);
672+
673+
uint256 signerPrivateKey = _getPrivateKeyForAddress(owner);
674+
bytes32 messageHash = metaTx.message;
675+
bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
676+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, ethSignedMessageHash);
677+
metaTx.signature = abi.encodePacked(r, s, v);
678+
679+
vm.prank(broadcaster);
680+
accountBlox.guardConfigBatchRequestAndApprove(metaTx);
681+
}
682+
683+
/**
684+
* @dev Helper to grant owner REQUEST+APPROVE permission for a function selector
685+
*/
686+
function _grantOwnerPermission(bytes4 functionSelector, EngineBlox.TxAction[] memory actions) internal {
687+
string memory roleName = "COMPOSITE_EXECUTE_ROLE";
688+
bytes32 roleHash = keccak256(bytes(roleName));
689+
690+
// 1. Create role without permissions
691+
IRuntimeRBAC.RoleConfigAction[] memory createActions = new IRuntimeRBAC.RoleConfigAction[](1);
692+
EngineBlox.FunctionPermission[] memory emptyPermissions = new EngineBlox.FunctionPermission[](0);
693+
createActions[0] = IRuntimeRBAC.RoleConfigAction({
694+
actionType: IRuntimeRBAC.RoleConfigActionType.CREATE_ROLE,
695+
data: abi.encode(roleName, 10, emptyPermissions)
696+
});
697+
bytes memory createParams = RuntimeRBACDefinitions.roleConfigBatchExecutionParams(abi.encode(createActions));
698+
EngineBlox.MetaTransaction memory createMetaTx = _createMetaTxForRoleConfig(
699+
owner,
700+
createParams,
701+
1 hours
702+
);
703+
vm.prank(broadcaster);
704+
accountBlox.roleConfigBatchRequestAndApprove(createMetaTx);
705+
706+
// 2. Add owner wallet to the role
707+
IRuntimeRBAC.RoleConfigAction[] memory addWalletActions = new IRuntimeRBAC.RoleConfigAction[](1);
708+
addWalletActions[0] = IRuntimeRBAC.RoleConfigAction({
709+
actionType: IRuntimeRBAC.RoleConfigActionType.ADD_WALLET,
710+
data: abi.encode(roleHash, owner)
711+
});
712+
bytes memory addWalletParams = RuntimeRBACDefinitions.roleConfigBatchExecutionParams(abi.encode(addWalletActions));
713+
EngineBlox.MetaTransaction memory addWalletMetaTx = _createMetaTxForRoleConfig(
714+
owner,
715+
addWalletParams,
716+
1 hours
717+
);
718+
vm.prank(broadcaster);
719+
accountBlox.roleConfigBatchRequestAndApprove(addWalletMetaTx);
720+
721+
// 3. Add function permission for the selector
722+
bytes4[] memory handlerForSelectors = new bytes4[](1);
723+
handlerForSelectors[0] = functionSelector;
724+
EngineBlox.FunctionPermission memory permission = EngineBlox.FunctionPermission({
725+
functionSelector: functionSelector,
726+
grantedActionsBitmap: EngineBlox.createBitmapFromActions(actions),
727+
handlerForSelectors: handlerForSelectors
728+
});
729+
IRuntimeRBAC.RoleConfigAction[] memory addPermActions = new IRuntimeRBAC.RoleConfigAction[](1);
730+
addPermActions[0] = IRuntimeRBAC.RoleConfigAction({
731+
actionType: IRuntimeRBAC.RoleConfigActionType.ADD_FUNCTION_TO_ROLE,
732+
data: abi.encode(roleHash, permission)
733+
});
734+
bytes memory addPermParams = RuntimeRBACDefinitions.roleConfigBatchExecutionParams(abi.encode(addPermActions));
735+
EngineBlox.MetaTransaction memory addPermMetaTx = _createMetaTxForRoleConfig(
736+
owner,
737+
addPermParams,
738+
1 hours
739+
);
740+
vm.prank(broadcaster);
741+
accountBlox.roleConfigBatchRequestAndApprove(addPermMetaTx);
742+
}
573743
}

0 commit comments

Comments
 (0)