Skip to content

Commit 8645f1e

Browse files
authored
Merge pull request #142 from PracticalParticle/dev
Dev
2 parents dad9c5a + 5767433 commit 8645f1e

30 files changed

Lines changed: 327 additions & 89 deletions

abi/GuardControllerDefinitions.abi.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@
5151
"stateMutability": "view",
5252
"type": "function"
5353
},
54+
{
55+
"inputs": [],
56+
"name": "CONTROLLER_CONFIG_OPERATION",
57+
"outputs": [
58+
{
59+
"internalType": "bytes32",
60+
"name": "",
61+
"type": "bytes32"
62+
}
63+
],
64+
"stateMutability": "view",
65+
"type": "function"
66+
},
5467
{
5568
"inputs": [],
5669
"name": "CONTROLLER_OPERATION",

contracts/core/access/RuntimeRBAC.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ abstract contract RuntimeRBAC is BaseStateMachine, IRuntimeRBAC {
134134
function _executeRoleConfigBatch(IRuntimeRBAC.RoleConfigAction[] calldata actions) internal {
135135
_validateBatchSize(actions.length);
136136

137-
for (uint256 i = 0; i < actions.length; i++) {
137+
for (uint256 i = 0; i < actions.length; ++i) {
138138
IRuntimeRBAC.RoleConfigAction calldata action = actions[i];
139139

140140
if (action.actionType == IRuntimeRBAC.RoleConfigActionType.CREATE_ROLE) {

contracts/core/execution/lib/definitions/GuardControllerDefinitions.sol

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import "../../interface/IGuardController.sol";
1616
* and role permissions for GuardController's public execution functions.
1717
*
1818
* Key Features:
19-
* - Registers all 6 GuardController public execution functions
19+
* - Registers all 9 GuardController public execution functions
2020
* - Defines role permissions for OWNER_ROLE and BROADCASTER_ROLE
2121
* - Supports time-delay and meta-transaction workflows
2222
* - Matches EngineBloxDefinitions pattern for consistency
@@ -33,6 +33,8 @@ library GuardControllerDefinitions {
3333

3434
// Operation Type Constants
3535
bytes32 public constant CONTROLLER_OPERATION = keccak256("CONTROLLER_OPERATION");
36+
// Guard config batch only (whitelist / register-unregister function); distinct execution operation type bitmap.
37+
bytes32 public constant CONTROLLER_CONFIG_OPERATION = keccak256("CONTROLLER_CONFIG_OPERATION");
3638

3739
// Function Selector Constants
3840
// GuardController: executeWithTimeLock(address,uint256,bytes4,bytes,uint256,bytes32)
@@ -86,7 +88,7 @@ library GuardControllerDefinitions {
8688
*
8789
* Function schemas define:
8890
* - GuardController public execution functions
89-
* - What operation types they belong to (CONTROLLER_OPERATION)
91+
* - What operation types they belong to (CONTROLLER_OPERATION vs CONTROLLER_CONFIG_OPERATION)
9092
* - What actions are supported (time-delay request/approve/cancel, meta-tx approve/cancel/request-and-approve)
9193
* - Whether they are protected
9294
*
@@ -96,7 +98,7 @@ library GuardControllerDefinitions {
9698
* - Role permissions are defined in getRolePermissions() matching EngineBloxDefinitions pattern
9799
*/
98100
function getFunctionSchemas() public pure returns (EngineBlox.FunctionSchema[] memory) {
99-
EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](8);
101+
EngineBlox.FunctionSchema[] memory schemas = new EngineBlox.FunctionSchema[](9);
100102

101103
// ============ TIME-DELAY WORKFLOW ACTIONS ============
102104
// Request action for executeWithTimeLock
@@ -144,6 +146,9 @@ library GuardControllerDefinitions {
144146
requestAndApproveExecutionHandlerForSelectors[0] = REQUEST_AND_APPROVE_EXECUTION_SELECTOR;
145147
bytes4[] memory guardConfigBatchExecuteHandlerForSelectors = new bytes4[](1);
146148
guardConfigBatchExecuteHandlerForSelectors[0] = GUARD_CONFIG_BATCH_EXECUTE_SELECTOR;
149+
150+
bytes4[] memory executeWithPaymentHandlerForSelectors = new bytes4[](1);
151+
executeWithPaymentHandlerForSelectors[0] = EXECUTE_WITH_PAYMENT_SELECTOR;
147152

148153
// Handler selectors point to execution selectors
149154
bytes4[] memory guardConfigHandlerForSelectors = new bytes4[](1);
@@ -225,8 +230,8 @@ library GuardControllerDefinitions {
225230
schemas[6] = EngineBlox.FunctionSchema({
226231
functionSignature: "guardConfigBatchRequestAndApprove(((uint256,uint256,uint8,(address,address,uint256,uint256,bytes32,bytes4,bytes),bytes32,bytes,(address,uint256,address,uint256)),(uint256,uint256,address,bytes4,uint8,uint256,uint256,address),bytes32,bytes,bytes))",
227232
functionSelector: GUARD_CONFIG_BATCH_META_SELECTOR,
228-
operationType: CONTROLLER_OPERATION,
229-
operationName: "CONTROLLER_OPERATION",
233+
operationType: CONTROLLER_CONFIG_OPERATION,
234+
operationName: "CONTROLLER_CONFIG_OPERATION",
230235
supportedActionsBitmap: EngineBlox.createBitmapFromActions(metaTxRequestApproveActions),
231236
enforceHandlerRelations: true,
232237
isProtected: true,
@@ -241,14 +246,27 @@ library GuardControllerDefinitions {
241246
schemas[7] = EngineBlox.FunctionSchema({
242247
functionSignature: "executeGuardConfigBatch((uint8,bytes)[])",
243248
functionSelector: GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
244-
operationType: CONTROLLER_OPERATION,
245-
operationName: "CONTROLLER_OPERATION",
249+
operationType: CONTROLLER_CONFIG_OPERATION,
250+
operationName: "CONTROLLER_CONFIG_OPERATION",
246251
supportedActionsBitmap: EngineBlox.createBitmapFromActions(guardConfigExecutionActions),
247252
enforceHandlerRelations: false,
248253
isProtected: true,
249254
handlerForSelectors: guardConfigBatchExecuteHandlerForSelectors
250255
});
251256

257+
// Schema 8: GuardController.executeWithPayment (same time-delay request action as executeWithTimeLock;
258+
// OWNER_ROLE grant for this selector may be added manually if the flow is enabled)
259+
schemas[8] = EngineBlox.FunctionSchema({
260+
functionSignature: "executeWithPayment(address,uint256,bytes4,bytes,uint256,bytes32,(address,uint256,address,uint256))",
261+
functionSelector: EXECUTE_WITH_PAYMENT_SELECTOR,
262+
operationType: CONTROLLER_OPERATION,
263+
operationName: "CONTROLLER_OPERATION",
264+
supportedActionsBitmap: EngineBlox.createBitmapFromActions(timeDelayRequestActions),
265+
enforceHandlerRelations: false,
266+
isProtected: true,
267+
handlerForSelectors: executeWithPaymentHandlerForSelectors
268+
});
269+
252270
return schemas;
253271
}
254272

contracts/core/lib/EngineBlox.sol

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,16 @@ library EngineBlox {
376376
// Validate both execution and handler selector permissions (same as txRequest)
377377
_validateExecutionAndHandlerPermissions(self, msg.sender, executionSelector, handlerSelector, TxAction.EXECUTE_TIME_DELAY_REQUEST);
378378

379+
// Request-time validation for attached payment details.
380+
// This prevents creating persistent PENDING records that later fail during
381+
// `executeAttachedPayment` due to missing/zero payment fields.
382+
if (paymentDetails.nativeTokenAmount > 0 || paymentDetails.erc20TokenAmount > 0) {
383+
SharedValidation.validateNotZeroAddress(paymentDetails.recipient);
384+
}
385+
if (paymentDetails.erc20TokenAmount > 0) {
386+
SharedValidation.validateNotZeroAddress(paymentDetails.erc20TokenAddress);
387+
}
388+
379389
return _txRequest(
380390
self,
381391
requester,
@@ -574,6 +584,16 @@ library EngineBlox {
574584
// Validate both execution and handler selector permissions
575585
_validateExecutionAndHandlerPermissions(self, msg.sender, metaTx.txRecord.params.executionSelector, metaTx.params.handlerSelector, TxAction.EXECUTE_META_REQUEST_AND_APPROVE);
576586

587+
// Request-time validation for attached payment details.
588+
// `requestAndApprove` creates the request and executes via the same meta-tx flow,
589+
// so we validate here to avoid persisting bad PENDING records.
590+
if (metaTx.txRecord.payment.nativeTokenAmount > 0 || metaTx.txRecord.payment.erc20TokenAmount > 0) {
591+
SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.recipient);
592+
}
593+
if (metaTx.txRecord.payment.erc20TokenAmount > 0) {
594+
SharedValidation.validateNotZeroAddress(metaTx.txRecord.payment.erc20TokenAddress);
595+
}
596+
577597
TxRecord memory txRecord = _txRequest(
578598
self,
579599
metaTx.txRecord.params.requester,

contracts/core/pattern/Account.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
pragma solidity 0.8.34;
33

44
import "../execution/GuardController.sol";
5+
import "../execution/interface/IGuardController.sol";
56
import "../access/RuntimeRBAC.sol";
7+
import "../access/interface/IRuntimeRBAC.sol";
68
import "../security/SecureOwnable.sol";
9+
import "../security/interface/ISecureOwnable.sol";
710
import "../lib/utils/SharedValidation.sol";
811

912
/**
@@ -51,9 +54,15 @@ abstract contract Account is GuardController, RuntimeRBAC, SecureOwnable {
5154

5255
/**
5356
* @dev See {IERC165-supportsInterface}.
57+
* @notice GuardController, RuntimeRBAC, and SecureOwnable each extend BaseStateMachine directly; a single
58+
* `super` chain only walks one branch. We OR the three component interface IDs here, then delegate
59+
* once to `super` for IBaseStateMachine / ERC165 — avoids tripling BaseStateMachine+ERC165 work.
5460
*/
5561
function supportsInterface(bytes4 interfaceId) public view virtual override(GuardController, RuntimeRBAC, SecureOwnable) returns (bool) {
56-
return GuardController.supportsInterface(interfaceId) || RuntimeRBAC.supportsInterface(interfaceId) || SecureOwnable.supportsInterface(interfaceId);
62+
return interfaceId == type(IGuardController).interfaceId
63+
|| interfaceId == type(IRuntimeRBAC).interfaceId
64+
|| interfaceId == type(ISecureOwnable).interfaceId
65+
|| super.supportsInterface(interfaceId);
5766
}
5867

5968
/**

contracts/core/security/SecureOwnable.sol

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,20 @@ import "./interface/ISecureOwnable.sol";
2727
* Each operation follows a request -> approval workflow with appropriate time locks
2828
* and authorization checks. Operations can be cancelled within specific time windows.
2929
*
30-
* At most one ownership-transfer or broadcaster-update request may be pending at a time:
31-
* a pending request of either type blocks new requests until it is approved or cancelled.
30+
* Pending secure requests use separate flags for ownership transfer and broadcaster update.
31+
* A new ownership-transfer request is allowed if no ownership transfer is already pending
32+
* (a broadcaster update may still be pending). A new broadcaster-update request is allowed only
33+
* when neither type has a pending request.
3234
*
3335
* This contract focuses purely on security logic while leveraging the BaseStateMachine
3436
* for transaction management, meta-transactions, and state machine operations.
3537
*/
3638
abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
3739
using SharedValidation for *;
3840

39-
/// @dev True while any pending ownership transfer or broadcaster update request exists; blocks new requests until handled.
40-
bool private _hasOpenRequest;
41+
/// @dev Tracks pending secure txs by type. Upgrading from legacy `_hasOpenRequest` / `_pendingBits` requires no pending requests.
42+
bool private _hasOpenOwnershipRequest;
43+
bool private _hasOpenBroadcasterRequest;
4144

4245
/**
4346
* @notice Initializer to initialize SecureOwnable state
@@ -83,7 +86,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
8386
*/
8487
function transferOwnershipRequest() public returns (uint256 txId) {
8588
SharedValidation.validateRecovery(getRecovery());
86-
_requireNoPendingRequest();
89+
_requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
8790

8891
EngineBlox.TxRecord memory txRecord = _requestTransaction(
8992
msg.sender,
@@ -95,7 +98,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
9598
abi.encode(getRecovery())
9699
);
97100

98-
_hasOpenRequest = true;
101+
_hasOpenOwnershipRequest = true;
99102
_logAddressPairEvent(owner(), getRecovery());
100103
return txRecord.txId;
101104
}
@@ -143,13 +146,15 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
143146
// Broadcaster Management
144147
/**
145148
* @dev Requests an update to the broadcaster at a specific location (index).
149+
* @notice Requires no pending broadcaster-update and no pending ownership-transfer request.
146150
* @param newBroadcaster The new broadcaster address (zero address to revoke at location)
147151
* @param location The index in the broadcaster role's authorized wallets set
148152
* @return txId The transaction ID for the pending request (use getTransaction(txId) for full record)
149153
*/
150154
function updateBroadcasterRequest(address newBroadcaster, uint256 location) public returns (uint256 txId) {
151155
SharedValidation.validateOwner(owner());
152-
_requireNoPendingRequest();
156+
_requireNoPendingRequest(SecureOwnableDefinitions.BROADCASTER_UPDATE);
157+
_requireNoPendingRequest(SecureOwnableDefinitions.OWNERSHIP_TRANSFER);
153158

154159
// Get the current broadcaster at the specified location. zero address if no broadcaster at location.
155160
address currentBroadcaster = location < _getSecureState().roles[EngineBlox.BROADCASTER_ROLE].walletCount
@@ -166,7 +171,7 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
166171
abi.encode(newBroadcaster, location)
167172
);
168173

169-
_hasOpenRequest = true;
174+
_hasOpenBroadcasterRequest = true;
170175
_logAddressPairEvent(currentBroadcaster, newBroadcaster);
171176
return txRecord.txId;
172177
}
@@ -281,12 +286,6 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
281286

282287
// ============ INTERNAL FUNCTIONS ============
283288

284-
/**
285-
* @dev Reverts if an ownership-transfer or broadcaster-update request is already pending.
286-
*/
287-
function _requireNoPendingRequest() internal view {
288-
if (_hasOpenRequest) revert SharedValidation.PendingSecureRequest();
289-
}
290289

291290
/**
292291
* @dev Validates that the caller is the broadcaster and that the meta-tx signer is the owner.
@@ -298,25 +297,56 @@ abstract contract SecureOwnable is BaseStateMachine, ISecureOwnable {
298297
}
299298

300299
/**
301-
* @dev Completes ownership/broadcaster flow after approval: resets flag and returns txId.
300+
* @dev Completes ownership/broadcaster flow after approval: clears the matching pending flag and returns txId.
302301
* @param updatedRecord The updated transaction record from approval
303302
* @return txId The transaction ID
304303
*/
305304
function _completeApprove(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
306-
_hasOpenRequest = false;
305+
_clearPendingFlagForOperation(updatedRecord.params.operationType);
307306
return updatedRecord.txId;
308307
}
309308

310309
/**
311-
* @dev Completes ownership/broadcaster flow after cancellation: resets flag, logs txId, returns txId.
310+
* @dev Completes ownership/broadcaster flow after cancellation: clears the matching pending flag and returns txId.
312311
* @param updatedRecord The updated transaction record from cancellation
313312
* @return txId The transaction ID
314313
*/
315314
function _completeCancel(EngineBlox.TxRecord memory updatedRecord) internal returns (uint256 txId) {
316-
_hasOpenRequest = false;
315+
_clearPendingFlagForOperation(updatedRecord.params.operationType);
317316
return updatedRecord.txId;
318317
}
319318

319+
/**
320+
* @dev Reverts if the pending flag for `requestOperationType` is already set (one lane per call).
321+
* `OWNERSHIP_TRANSFER` checks only `_hasOpenOwnershipRequest` (a broadcaster update may still be pending).
322+
* `BROADCASTER_UPDATE` checks only `_hasOpenBroadcasterRequest`. Callers that need both lanes idle
323+
* (e.g. `updateBroadcasterRequest`) invoke this once per operation type.
324+
* @param requestOperationType Lane to validate (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
325+
*/
326+
function _requireNoPendingRequest(bytes32 requestOperationType) internal view {
327+
if (requestOperationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
328+
if (_hasOpenOwnershipRequest) revert SharedValidation.PendingSecureRequest();
329+
} else if (requestOperationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
330+
if (_hasOpenBroadcasterRequest) revert SharedValidation.PendingSecureRequest();
331+
} else {
332+
revert();
333+
}
334+
}
335+
336+
/**
337+
* @dev Clears the pending flag for a completed or cancelled secure op (approve/cancel paths).
338+
* @param operationType The tx record's `operationType` (`OWNERSHIP_TRANSFER` or `BROADCASTER_UPDATE`).
339+
*/
340+
function _clearPendingFlagForOperation(bytes32 operationType) private {
341+
if (operationType == SecureOwnableDefinitions.OWNERSHIP_TRANSFER) {
342+
_hasOpenOwnershipRequest = false;
343+
} else if (operationType == SecureOwnableDefinitions.BROADCASTER_UPDATE) {
344+
_hasOpenBroadcasterRequest = false;
345+
} else {
346+
revert();
347+
}
348+
}
349+
320350
/**
321351
* @dev Transfers ownership of the contract
322352
* @param newOwner The new owner of the contract

docs/core/execution/lib/definitions/GuardControllerDefinitions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Returns predefined function schemas for GuardController execution functions
4141

4242
Function schemas define:
4343
- GuardController public execution functions
44-
- What operation types they belong to (CONTROLLER_OPERATION)
44+
- What operation types they belong to (`CONTROLLER_OPERATION` for execution paths, `CONTROLLER_CONFIG_OPERATION` for guard config batch)
4545
- What actions are supported (time-delay request/approve/cancel, meta-tx approve/cancel/request-and-approve)
4646
- Whether they are protected
4747

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Bloxchain",
3-
"version": "1.0.0-alpha.18",
3+
"version": "1.0.0-alpha.19",
44
"description": "Library engine for building enterprise grade decentralized permissioned applications",
55
"type": "module",
66
"main": "truffle-config.cjs",

package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bloxchain/contracts",
3-
"version": "1.0.0-alpha.18",
3+
"version": "1.0.0-alpha.19",
44
"description": "Library engine for building enterprise grade decentralized permissioned applications",
55
"files": [
66
"core",

scripts/sanity-sdk/guard-controller/base-test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export abstract class BaseGuardControllerTest extends BaseSDKTest {
6464
protected metaTxSigner: MetaTransactionSigner | null = null;
6565

6666
// GuardController constants
67-
protected readonly CONTROLLER_OPERATION_TYPE: Hex = keccak256(new TextEncoder().encode('CONTROLLER_OPERATION')) as Hex;
67+
protected readonly CONTROLLER_CONFIG_OPERATION_TYPE: Hex = keccak256(
68+
new TextEncoder().encode('CONTROLLER_CONFIG_OPERATION')
69+
) as Hex;
6870
protected readonly GUARD_CONFIG_BATCH_META_SELECTOR: Hex = keccak256(
6971
new TextEncoder().encode('guardConfigBatchRequestAndApprove(((uint256,uint256,uint8,(address,address,uint256,uint256,bytes32,bytes4,bytes),bytes32,bytes,(address,uint256,address,uint256)),(uint256,uint256,address,bytes4,uint8,uint256,uint256,address),bytes32,bytes,bytes))')
7072
).slice(0, 10) as Hex;
@@ -407,7 +409,7 @@ export abstract class BaseGuardControllerTest extends BaseSDKTest {
407409
target: this.contractAddress!,
408410
value: BigInt(0),
409411
gasLimit: BigInt(1000000),
410-
operationType: this.CONTROLLER_OPERATION_TYPE,
412+
operationType: this.CONTROLLER_CONFIG_OPERATION_TYPE,
411413
executionSelector: this.GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
412414
executionParams: executionParams
413415
};
@@ -493,7 +495,7 @@ export abstract class BaseGuardControllerTest extends BaseSDKTest {
493495
target: this.contractAddress!,
494496
value: BigInt(0),
495497
gasLimit: BigInt(1000000),
496-
operationType: this.CONTROLLER_OPERATION_TYPE,
498+
operationType: this.CONTROLLER_CONFIG_OPERATION_TYPE,
497499
executionSelector: this.GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
498500
executionParams: executionParams
499501
};
@@ -559,13 +561,13 @@ export abstract class BaseGuardControllerTest extends BaseSDKTest {
559561
signerWallet.address
560562
);
561563

562-
// TxParams for controller operation (mirrors createSignedMetaTxForFunctionRegistration)
564+
// TxParams for controller config operation (mirrors createSignedMetaTxForFunctionRegistration)
563565
const txParams = {
564566
requester: signerWallet.address,
565567
target: this.contractAddress,
566568
value: BigInt(0),
567569
gasLimit: BigInt(1_000_000),
568-
operationType: this.CONTROLLER_OPERATION_TYPE,
570+
operationType: this.CONTROLLER_CONFIG_OPERATION_TYPE,
569571
executionSelector: this.GUARD_CONFIG_BATCH_EXECUTE_SELECTOR,
570572
executionParams,
571573
};

0 commit comments

Comments
 (0)