SO4.market uses a role-based access control system managed by the role_store contract. Every privileged operation checks role membership before proceeding; unauthorised callers panic with Unauthorized.
| Role constant | Key seed | Grantor | Typical holder | Permitted actions |
|---|---|---|---|---|
ROLE_ADMIN |
"ROLE_ADMIN" |
Self (bootstrap) | Protocol multisig / deployer | Grant and revoke any role in role_store |
CONTROLLER |
"CONTROLLER" |
ROLE_ADMIN |
Core contracts (oracle, order_handler, etc.) | Write to data_store — prices, OI, pool amounts, flags |
MARKET_KEEPER |
"MARKET_KEEPER" |
ROLE_ADMIN |
Market creation bot | Create and configure new markets in data_store |
ORDER_KEEPER |
"ORDER_KEEPER" |
ROLE_ADMIN |
Execution keeper bots | Submit prices via oracle.set_prices; execute pending orders |
LIQUIDATION_KEEPER |
"LIQUIDATION_KEEPER" |
ROLE_ADMIN |
Liquidation bots | Call liquidation_handler.liquidate_position |
ADL_KEEPER |
"ADL_KEEPER" |
ROLE_ADMIN |
ADL bots | Call adl_handler.execute_adl |
FEE_KEEPER |
"FEE_KEEPER" |
ROLE_ADMIN |
Fee distribution account | Claim and distribute protocol fees |
Note on contract upgrades:
ROLE_ADMINonly gatesgrant_roleandrevoke_roleinsiderole_store. Contract upgrades (fn upgrade) are not routed throughrole_storeat all — each upgradeable contract stores its own admin address in its own instance storage and callsadmin.require_auth()directly. Revoking or rotating arole_storerole has no effect on any contract's upgrade authority.
The
BytesN<32>key stored inrole_storeissha256(role_name_utf8_bytes). Usescripts/compute_key.py role <ROLE_NAME>to derive the hex key for any role name.
Role keys are computed in libs/gmx_keys/src/lib.rs under the roles module:
pub fn order_keeper(env: &Env) -> BytesN<32> {
env.crypto().sha256(&Bytes::from_slice(env, b"ORDER_KEEPER")).into()
}Every role follows the same pattern: sha256(UPPERCASE_ROLE_NAME_BYTES).
Any contract can verify role membership synchronously:
RoleStoreClient::new(env, &role_store).has_role(&account, &role_key)
// returns bool — true if account holds the roleRequires the caller to hold ROLE_ADMIN. Granting a role emits a RoleGranted event.
stellar contract invoke \
--id "$ROLE_STORE" \
--source "$ADMIN_SOURCE" \
--network testnet \
-- grant_role \
--caller "$ADMIN_ADDRESS" \
--account "$ACCOUNT_TO_GRANT" \
--role "$(python3 scripts/compute_key.py role ORDER_KEEPER)"Replace ORDER_KEEPER with any role name from the table above.
Requires the caller to hold ROLE_ADMIN. Revoking a role emits a RoleRevoked event.
stellar contract invoke \
--id "$ROLE_STORE" \
--source "$ADMIN_SOURCE" \
--network testnet \
-- revoke_role \
--caller "$ADMIN_ADDRESS" \
--account "$ACCOUNT_TO_REVOKE" \
--role "$(python3 scripts/compute_key.py role ORDER_KEEPER)"role_store emits an event on every successful grant or revoke:
| Event topic | Payload | When |
|---|---|---|
RoleGranted |
{ account: Address, role: BytesN<32> } |
After a successful grant_role call |
RoleRevoked |
{ account: Address, role: BytesN<32> } |
After a successful revoke_role call |
On a fresh deployment, initialise roles in this order:
- Deploy
role_store— callinitialize(admin). The admin address can now grant all other roles. - Deploy
data_store— callinitialize(admin, role_store). - Deploy core contracts — oracle, order_handler, liquidation_handler, adl_handler, etc.
- Grant
CONTROLLERto oracle and order_handler (they write todata_store). - Grant
ORDER_KEEPERto keeper bot accounts. - Grant
LIQUIDATION_KEEPERandADL_KEEPERto their respective bots. - Grant
FEE_KEEPERto the fee distribution account. - Optionally transfer
ROLE_ADMINto a multisig after all roles are configured.
Keeping the ROLE_ADMIN key in a hardware wallet or multisig is strongly recommended — all other roles are granted through it.