Platform: Code4rena
Start Date: 13/10/2023
Pot Size: $31,250 USDC
Total HM: 4
Participants: 51
Period: 7 days
Judge: 0xsomeone
Id: 295
League: ETH
Rank: 12/51
Findings: 1
Award: $152.13
đ Selected for report: 0
đ Solo Findings: 0
đ Selected for report: immeas
Also found by: Alra, Arz, GREY-HAWK-REACH, alexzoid, radev_sw, rvierdiiev, sorrynotsorry
152.1324 USDC - $152.13
Project Name | Brahma |
Repository | https://github.com/code-423n4/2023-10-brahma |
Website | Link |
Link | |
Methods | Manual Review |
Total nSLOC | 833 over 16 contracts |
ID | Title | Instances | Severity |
---|---|---|---|
L-01 | Bypassing of check in _ensureAddressProvider() function | - | Low |
L-02 | Console Account Creation Without Policies Leads to Enforcement Challenges | - | Low |
NC-01 | Add isWallet check for provided wallet address in WalletRegistry#registerSubAccount() function | - | Non Critical |
NC-02 | Outdated Documentation | - | Non Critical |
NC-03 | Consider to Implement Maximum SubAccount Limit | - | Non Critical |
NC-04 | Inconsistency in using Enumerable.AddressSet and address[] | - | Non Critical |
NC-05 | Inconsistency in Checking Ownership in ExecutorRegistry.sol | - | Non Critical |
NC-06 | Immutability of Registry Addresses in AddressProvider | - | Non Critical |
NC-07 | Inability for Console Account to Remove policyCommit for his SubAccounts | - | Non Critical |
NC-08 | Adding of Additional Checks in After Execution Functions in the Protocol | - | Non Critical |
_ensureAddressProvider()
function</a>[L-01]https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/AddressProvider.sol#L77-L90 https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/AddressProvider.sol#L97-L105
authorizedAddresses
mapping, gaining access to functionalities within the protocol that they should not have.. (kind of centralization risk)The AddressProvider
contract serves as a central source for resolving addresses of core components and external contracts within the Brahma DeFi Protocol. It provides governance-controlled mechanisms for setting authorized addresses and registry addresses used within the protocol.
The setRegistry()
and setAuthorizedAddress()
functions in the AddressProvider
contract serve important roles in managing the addresses of core components and authorized contracts within the Brahma DeFi Protocol.
The check in _ensureAddressProvider()
function during setRegistry()
and setAuthorizedAddress()
functions execution in AddressProvider.sol
contract can be bypassed.
setRegistry()
function allows the governance of the AddressProvider
contract to set the address of a core component or registry by specifying a unique _key
associated with that component. The _key
is used as a reference to access the registered address later.
setAuthorizedAddress()
function allows the governance to set an authorized address that can be used within the protocol. Authorized addresses are associated with a unique _key
and are stored in the authorizedAddresses
mapping.
Both functions utilize the _ensureAddressProvider()
function, which ensures that the new address supports the AddressProviderService
interface and is pointing to the AddressProvider
contract.
However, this function doesn't provide adequate protection. The check within the _ensureAddressProvider()
function can be bypassed by calling the setRegistry()
function with the _registry
parameter set to a contract that simply inherits IAddressProviderService
and has implemented the addressProviderTarget()
function, returning the address of the AddressProvider
contract. The same applies to the setAuthorizedAddress()
function with the _authorizedAddress
parameter.
function setAuthorizedAddress(bytes32 _key, address _authorizedAddress, bool _overrideCheck) external { _onlyGov(); _notNull(_authorizedAddress); /// @dev skips checks for supported `addressProvider()` if `_overrideCheck` is true if (!_overrideCheck) { /// @dev skips checks for supported `addressProvider()` if `_authorizedAddress` is an EOA if (_authorizedAddress.code.length != 0) _ensureAddressProvider(_authorizedAddress); } authorizedAddresses[_key] = _authorizedAddress; emit AuthorizedAddressInitialised(_authorizedAddress, _key); }
function setRegistry(bytes32 _key, address _registry) external { _onlyGov(); _ensureAddressProvider(_registry); if (registries[_key] != address(0)) revert RegistryAlreadyExists(); registries[_key] = _registry; emit RegistryInitialised(_registry, _key); }
// Malicious contract attempting to bypass the _ensureAddressProvider check contract MaliciousContract { function addressProviderTarget() external view returns (address) { // Return the AddressProvider contract address return address(0xAddressProviderContractAddress); } }
Implement Additional Checks: Consider implementing additional checks or validation mechanisms to verify the authenticity and authorization of addresses being added as authorized addresses.
Creating Console Accounts
in the Brahma protocol with zero policies can lead to unexpected issues when attempting to enforce policies. The SafeDeployer.sol
contract allows for the creation of Console Accounts
without policies.
function deployConsoleAccount(address[] calldata _owners, uint256 _threshold, bytes32 _policyCommit, bytes32 _salt) external nonReentrant returns (address _safe) { bool _policyHashValid = _policyCommit != bytes32(0); _safe = _createSafe(_owners, _setupConsoleAccount(_owners, _threshold, _policyHashValid), _salt); if (_policyHashValid) { PolicyRegistry(AddressProviderService._getRegistry(_POLICY_REGISTRY_HASH)).updatePolicy( _safe, _policyCommit ); } emit ConsoleAccountDeployed(_safe); }
The Brahma protocol relies on Console Accounts
, which can be either EOA (Externally Owned Account) or Safe Multisig Accounts
, to manage Safe Sub Accounts
with configured policies. In the case of Safe Multisig Accounts
, policies can be enforced on the Console Account
using the SafeModeratorOverridable
guard. However, the SafeDeployer
contract permits the creation of Safe Console Accounts
with zero policies, resulting in unexpected behavior.
When attempting to enforce a policy on a Console Account
with zero policies, it's required to enable the SafeModeratorOverridable
guard. This operation is performed as a Safe Multisig Transaction
without interactions with the Brahma protocol. As a consequence, the SafeModeratorOverridable
guard can be enabled on a Console with zero policies.
Policies are checked using an external trusted validator, and the signature of the validator is verified by the PolicyValidator
contract. However, this contract reverts if the policy is not set (i.e., zero policy), leading to a denial of service for the console account.
function isPolicySignatureValid(address account, bytes32 transactionStructHash, bytes calldata signatures) public view returns (bool) { // Get policy hash from registry bytes32 policyHash = PolicyRegistry(AddressProviderService._getRegistry(_POLICY_REGISTRY_HASH)).commitments(account); if (policyHash == bytes32(0)) { revert NoPolicyCommit(); } // ...more code...
Exploit Scenario:
Console Account
aim to enforce a new policy while the current policy is set to zero.SafeModeratorOverridable
guard.Recommendation:
It is recommended to consider requiring a non-zero policy when creating a console account. Alternatively, provide a helper function that enables the SafeModeratorOverridable guard and requires a policy to be set. This would prevent the unexpected issues arising from Console Accounts
with zero policies.
isWallet
check for provided wallet address in WalletRegistry#registerSubAccount()
function</a>[NC-01]Currently the registerSubAccount()
function in WalletRegistry.sol
contract can be called only from SafeDeployer.sol
contract. This is done in SafeDeployer#deploySubAccount()
where the function check if the msg.sender
is a registered wallet:
function deploySubAccount( address[] calldata _owners, uint256 _threshold, bytes32 _policyCommit, bytes32 _salt ) external nonReentrant returns (address _subAcc) { // ...code... // Check if msg.sender is a registered wallet WalletRegistry _walletRegistry = WalletRegistry( AddressProviderService._getRegistry(_WALLET_REGISTRY_HASH) ); if (!_walletRegistry.isWallet(msg.sender)) revert NotWallet(); // ...code... // Register sub account to wallet _walletRegistry.registerSubAccount(msg.sender, _subAcc); // ...code... }
Future the logic in WalletRegistry#registerSubAccount()
function can be changed and maybe allow to be called not only from SafeDeployer
contract.
So, It's recommended to implement check in WalletRegistry#registerSubAccount()
function if the provided wallet
address is registered wallet.
https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/registries/WalletRegistry.sol#L49-L55 https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/src/core/SafeDeployer.sol#L82-L103
Update the documentation for the 'Console Guard Removal Transaction' section, which can be found at link, to align with the latest code implementation of the Brahma Protocol.
Currently, the 'Console Guard Removal Transaction' section includes a schema illustrating the flow of the 'Console Guard Removal Transaction.' However, it references the _isGuardBeingRemoved()
function, which no longer exists in the code. Instead, the code now implements the _isConsoleBeingOverridden()
function with some logic changes.
Make the necessary updates to the documentation to reflect these changes accurately.
function _isConsoleBeingOverriden( address _from, address _to, uint256 _value, bytes memory _data, Enum.Operation _operation ) internal pure returns (bool) { /** * Following conditions validate if the transaction aims to remove guard or fallback handler on Safe * from == to (safe sending txn to itself) * value == 0 * data == abi.encodeCall(IGnosisSafe.setGuard, (address(0))) || abi.encodeCall(IGnosisSafe.setFallbackHandler, (address(0))) * operation == Enum.Operation.Call * * In case these conditions are met, the guard is being removed, return true */ if (_from == _to && _value == 0 && _operation == Enum.Operation.Call) { if (SafeHelper._GUARD_REMOVAL_CALLDATA_HASH == keccak256(_data)) { return true; } else if (SafeHelper._FALLBACK_REMOVAL_CALLDATA_HASH == keccak256(_data)) { return true; } } return false; }
Reference: L-01 issue from previous Brahma audit (https://github.com/Brahma-fi/brahma-security/blob/master/audits/brahma-fi-consolev2-audit-10-23-ackee.pdf).
SubAccount
Limit</a>[NC-03]The Brahma Protocol currently lacks a maximum limit on the number of sub-accounts that a wallet can create. This omission raises concerns about scalability, complexity, resource consumption, and security. It is recommended to establish a reasonable maximum limit for sub-accounts to address these issues.
Details:
Scalability Concerns: The absence of a sub-account limit may hinder protocol scalability, impacting performance.
Complexity: Managing numerous sub-accounts can become complex and may lead to operational and security challenges.
Resource Usage: Excessive sub-accounts consume blockchain resources, potentially affecting overall efficiency.
Security Risks: A large number of sub-accounts increase the attack surface, potentially compromising security.
Recommendation:
Implement a well-defined maximum limit on sub-accounts for each wallet to enhance scalability, security, and user experience.
Benefits:
Conclusion:
Enumerable.AddressSet
and address[]
</a>[NC-04]Links
Description:
In the Brahma Protocol smart contracts, specifically in the ExecutorRegistry.sol
and WalletRegistry.sol
contracts, there is an inconsistency in the data structures used for storing related information.
Examples:
In the ExecutorRegistry.sol
contract, the mapping subAccountToExecutors
is used to associate sub-accounts with a set of executor addresses using EnumerableSet.AddressSet
. This design choice ensures efficient membership checks and avoids duplicate entries.
Example:
mapping(address subAccount => EnumerableSet.AddressSet) private subAccountToExecutors;
On the other hand, in the WalletRegistry.sol
contract, the mapping walletToSubAccountList
is used to map wallet addresses to an array of sub-account addresses. Here, a simple address[]
array is used to store sub-account addresses.
Example:
mapping(address wallet => address[] subAccountList) public walletToSubAccountList;
Impact:
This inconsistency in data structure usage may lead to differences in contract behavior and may not fully leverage the advantages of Enumerable.AddressSet
. It could potentially impact contract efficiency, readability, and ease of use.
Recommendation:
For consistency and possibly better contract design, consider using Enumerable.AddressSet
consistently in both contracts (ExecutorRegistry.sol
and WalletRegistry.sol
) if the functionality allows. This will maintain a uniform approach to handling sets of addresses, provide efficient membership checks, and ensure clear and predictable behavior throughout the protocol.
ExecutorRegistry.sol
</a>[NC-05]Links:
Description:
In the Brahma Protocol smart contract ExecutorRegistry.sol
, there is an inconsistency in checking whether msg.sender
is the owner of a sub-account. This inconsistency exists in the registerExecutor()
and deRegisterExecutor()
functions.
Details:
In the registerExecutor()
function, the contract checks if msg.sender
is the owner of the sub-account using the following code:
WalletRegistry _walletRegistry = WalletRegistry( AddressProviderService._getRegistry(_WALLET_REGISTRY_HASH) ); if (!_walletRegistry.isOwner(msg.sender, _subAccount)) revert NotOwnerWallet();
Here, it verifies ownership by calling isOwner(msg.sender, _subAccount)
on the WalletRegistry
contract.
In the deRegisterExecutor()
function, the contract checks ownership using the following code:
WalletRegistry _walletRegistry = WalletRegistry( AddressProviderService._getRegistry(_WALLET_REGISTRY_HASH) ); if (_walletRegistry.subAccountToWallet(_subAccount) != msg.sender) revert NotOwnerWallet();
In this case, it checks ownership by comparing msg.sender
to subAccountToWallet(_subAccount)
from the WalletRegistry
contract.
Impact:
This inconsistency in checking ownership can lead to confusion and potential issues during contract interactions. Users may expect consistent ownership verification methods, but the contract uses different approaches in different functions.
Recommendation:
To maintain consistency and clarity in ownership checks, it is advisable to use a standardized method to verify ownership throughout the contract. You can choose one of the existing methods, such as isOwner(msg.sender, _subAccount)
, and use it consistently in both the registerExecutor()
and deRegisterExecutor()
functions.
Updating the code to use a single ownership verification method will help prevent potential misunderstandings and make the contract more user-friendly. Additionally, ensure that this change is well-documented in the contract's comments or documentation to provide clear guidance to users.
The AddressProvider contract enforces the immutability of registry addresses, meaning that once set, these addresses cannot be altered. This design choice has both positive and negative implications.
On the positive side, it enhances the trust model of the system as these addresses remain immutable, reducing the risk of malicious changes. However, in the unfortunate event of a disaster, this immutability becomes a limitation as it hinders potential recovery efforts for the protocol.
Users should be made aware of the consequences of this design choice, particularly its impact on the trust model. It is essential to provide clear information about the trade-offs between immutability and disaster recovery to ensure informed decision-making.
policyCommit
for his SubAccounts
</a>[NC-07]The PolicyRegistry
manages policy commits per account. The policy can be updated
SafeDeployer
msg.sender
is the owner of the account that is going to have an updated policymsg.sender
is a wallet and is updating the policy for itself.In the PolicyRegistry
contract of the Brahma Protocol, there exists a limitation where the commitments
for SubAccounts cannot be set to zero (removed) if the msg.sender is ConsoleAccount/Wallet
that is actually the owner of the SubAccount. This behavior is due to a check in the updatePolicy()
function that reverts if policyCommit
is set to bytes32(0)
. As a result, Console Accounts cannot remove (set to zero) the commitments
data for their SubAccounts, which may not be the expected behavior.
function updatePolicy(address account, bytes32 policyCommit) external { if (policyCommit == bytes32(0)) { revert PolicyCommitInvalid(); } // ... (other logic) } else if (walletRegistry.isOwner(msg.sender, account)) { //In case invoker is updating on behalf of sub account } // ... (other logic) _updatePolicy(account, policyCommit); }
The impact of this issue is that commitments
data of SubAccounts cannot be removed or set to zero bytes data by the ConsoleAccount/Wallet. This means that once a policyCommit
is set for a SubAccount, it cannot be effectively revoked or cleared. While this might be the intended behavior for some use cases, it limits the flexibility of Console Accounts to manage the policies of their SubAccounts.
Consider to allow bytes(0) for commitments[account]
to be set only if the msg.sender
is the Console Account/Wallet
that is going to change the commitments
data for his SubAccount
.
After Execution Functions
in the Protocol</a>[NC-08]Consider to enhance security by adding additional checks in the After Execution Functions
in the Protocol for various scenarios. These checks will help ensure the integrity and security of the protocol.
Example of validation checks that can be added in After Execution Functions:
nonce
that is created during ConsoleAccount Deployment is calculated with the Console Account Owners Hash
)Reference: Documentation Flow - https://github.com/code-423n4/2023-10-brahma/blob/main/contracts/docs/Architecture.md
#0 - c4-pre-sort
2023-10-22T23:08:01Z
raymondfam marked the issue as high quality report
#1 - 0xad1onchain
2023-10-25T14:58:34Z
ID | Title | Instances | Severity | Â |
---|---|---|---|---|
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#L-01 | Bypassing of check in _ensureAddressProvider() function | - | Low | NO, its a part of deployment. Incorrect deployments are not in scope |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#L-02 | Console Account Creation Without Policies Leads to Enforcement Challenges | - | Low | NO, no policy == no enforcement of policy |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-01 | Add isWallet check for provided wallet address in WalletRegistry#registerSubAccount() function | - | Non Critical | NO, check already performed in SafeDeployer |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-02 | Outdated Documentation | - | Non Critical | YES, fix method name in diagram |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-03 | Consider to Implement Maximum SubAccount Limit | - | Non Critical | NO |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-04 | Inconsistency in using Enumerable.AddressSet and address[] | - | Non Critical | NO, both are suited for their own ops |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-05 | Inconsistency in Checking Ownership in ExecutorRegistry.sol | - | Non Critical | YES, add isSubAccount() |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-06 | Immutability of Registry Addresses in AddressProvider | - | Non Critical | NO, desired design |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-07 | Inability for Console Account to Remove policyCommit for his SubAccounts | - | Non Critical | NO, desired design |
https://github.com/code-423n4/2023-10-brahma-findings/issues/439#NC-08 | Adding of Additional Checks in After Execution Functions in the Protocol | - | Non Critical | NO, only guard states are checked, so that the guard via backend validation can ensure all other state is in place |
#2 - c4-sponsor
2023-10-25T14:58:47Z
0xad1onchain (sponsor) acknowledged
#3 - alex-ppg
2023-10-27T12:06:18Z
The report is exceptional in layout and formatting, however, analysis of the report indicates that there are some incorrect findings as well as misconceptions in relation to the code of the Sponsor. As such, a grade of B will be assigned.
#4 - c4-judge
2023-10-27T12:06:24Z
alex-ppg marked the issue as grade-b
#5 - alex-ppg
2023-10-31T21:29:44Z
Based on the downgrade but acceptance of multiple QA findings, I will upgrade the grade of this QA report to A.
#6 - c4-judge
2023-10-31T21:29:49Z
alex-ppg marked the issue as grade-a