Hats.finance builds autonomous security infrastructure for integration with major DeFi protocols to secure users' assets. It aims to be the decentralized choice for Web3 security, offering proactive security mechanisms like decentralized audit competitions and bug bounties. The protocol facilitates audit competitions to quickly secure smart contracts by having auditors compete, thereby reducing auditing costs and accelerating submissions. This aligns with their mission of fostering a robust, secure, and scalable Web3 ecosystem through decentralized security solutions.
Hats Audit Competitions offer a unique and decentralized approach to enhancing the security of web3 projects. Leveraging the large collective expertise of hundreds of skilled auditors, these competitions foster a proactive bug hunting environment to fortify projects before their launch. Unlike traditional security assessments, Hats Audit Competitions operate on a time-based and results-driven model, ensuring that only successful auditors are rewarded for their contributions. This pay-for-results ethos not only allocates budgets more efficiently by paying exclusively for identified vulnerabilities but also retains funds if no issues are discovered. With a streamlined evaluation process, Hats prioritizes quality over quantity by rewarding the first submitter of a vulnerability, thus eliminating duplicate efforts and attracting top talent in web3 auditing. The process embodies Hats Finance's commitment to reducing fees, maintaining project control, and promoting high-quality security assessments, setting a new standard for decentralized security in the web3 space.
Palmera streamlines your Safes operations and treasury management across multiple chains all from a single dashboard
- Type: A public audit competition hosted by Palmera
- Duration: 2 weeks
- Maximum Reward: $29,883.36
- Submissions: 101
- Total Payout: $29,883.36 distributed among 30 participants.
├── src
│ ├── DenyHelper.sol
│ ├── Helpers.sol
│ ├── PalmeraGuard.sol
│ ├── PalmeraModule.sol
│ ├── PalmeraRoles.sol
│ ├── ReentrancyAttack.sol
│ ├── SafeInterfaces.sol
│ ├── SigningUtils.sol
│ ├── libraries
│ │ ├── Constants.sol
│ │ ├── DataTypes.sol
│ │ ├── Errors.sol
│ │ └── Events.sol
Note that contracts under the test, script and lib directories are explicitely not in scope for the audit competition.
-
Insufficient Access Control in execTransactionOnBehalf Due to Broad Lead Role Check
In the
execTransactionOnBehalf
function, there is a mechanism that allows bypassing signature verification if the caller has a Safe Lead role. However, this mechanism fails to differentiate between the various types of Safe Lead roles (SAFE_LEAD
,SAFE_LEAD_EXEC_ON_BEHALF_ONLY
, andSAFE_LEAD_MODIFY_OWNERS_ONLY
). This broad lead role check results in insufficient access control because it permits any lead role to execute transactions on behalf of the safe without needing a signature verification.
The issue arises due to the isSafeLead
function only verifying whether the caller has a general lead role, rather than distinguishing between specific roles. This general check is implemented through a simple role-checking mechanism that does not account for the exact nature of the lead role.
The proposed mitigation is to update the execTransactionOnBehalf
function to verify specifically for the SAFE_LEAD_EXEC_ON_BEHALF_ONLY
role before bypassing the signature verification. This adjustment would ensure more precise access control.
Additionally, while unrelated functions like addOwnerWithThreshold
and removeOwner
appear to be controlled by other mechanisms such as the Palmera Roles, the core concern remains with the execTransactionOnBehalf
function’s overly broad role-checking logic.
Link: Issue #31
-
Revocation of Multiple Safe Lead Roles Fails in disableSafeLeadRoles Function
The
disableSafeLeadRoles
function is intended to revoke specific Safe Lead roles from a user. However, its current implementation only revokes the first specified role and skips the others. This can lead to a situation where if a user holds multiple Safe Lead roles, such asSAFE_LEAD_EXEC_ON_BEHALF_ONLY
andSAFE_LEAD_MODIFY_OWNERS_ONLY
, the function will only revoke theSAFE_LEAD_EXEC_ON_BEHALF_ONLY
and leave theSAFE_LEAD_MODIFY_OWNERS_ONLY
role active. This results in insufficient role revocation, compromising the system’s integrity.
For example, if a user has both SAFE_LEAD_EXEC_ON_BEHALF_ONLY
and SAFE_LEAD_MODIFY_OWNERS_ONLY
, calling disableSafeLeadRoles
will only revoke the SAFE_LEAD_EXEC_ON_BEHALF_ONLY
role. Consequently, the remaining role, SAFE_LEAD_MODIFY_OWNERS_ONLY
, is not affected and remains active.
To mitigate this issue, the disableSafeLeadRoles
function should be updated to independently check and revoke all Safe Lead roles, regardless of their order in the conditional checks. The revised implementation ensures all roles are revoked as intended:
function disableSafeLeadRoles(address user) private {
RolesAuthority _authority = RolesAuthority(rolesAuthority);
if (_authority.doesUserHaveRole(user, uint8(DataTypes.Role.SAFE_LEAD))) {
_authority.setUserRole(user, uint8(DataTypes.Role.SAFE_LEAD), false);
}
if (_authority.doesUserHaveRole(user, uint8(DataTypes.Role.SAFE_LEAD_EXEC_ON_BEHALF_ONLY))) {
_authority.setUserRole(user, uint8(DataTypes.Role.SAFE_LEAD_EXEC_ON_BEHALF_ONLY), false);
}
if (_authority.doesUserHaveRole(user, uint8(DataTypes.Role.SAFE_LEAD_MODIFY_OWNERS_ONLY))) {
_authority.setUserRole(user, uint8(DataTypes.Role.SAFE_LEAD_MODIFY_OWNERS_ONLY), false);
}
}
Link: Issue #37
-
isSafeLead Function Fails to Verify Current Roles, Allowing Unauthorized Access
The
isSafeLead
function is designed to verify if a user is a lead for a specific safe by examining the_safe.lead
attribute. However, this mechanism fails to account for cases where a user's Safe Lead roles have been disabled via thedisableSafeLeadRoles
function. As a result, the function neglects to check the user's current roles within theRolesAuthority
contract, potentially leading to unauthorized access.
This issue can have serious implications, as it allows users whose roles have been revoked to still be recognized as safe leads. This oversight compromises the access control mechanism, enabling these users to perform unauthorized actions.
A proof of concept illustrates this problem:
- A user
X
is initially set as a lead forsafe B
. - The
disableSafeLeadRoles
function revokes all Safe Lead roles from userX
. - Despite the revocation, the
isSafeLead
function still identifiesX
as the lead forsafe B
because it only checks the_safe.lead
attribute, ignoring the user's current roles.
The current implementation of the isSafeLead
function needs to be enhanced to include a check against the RolesAuthority
to ensure that the user still has the relevant Safe Lead roles. This would prevent unauthorized access by users whose roles have been revoked.
Link: Issue #38
-
SetRole function doesn't consider safeId properly, allowing unrestricted role assignments
In the
setRole
function designed to assign specific roles to users for a given safe (identified bysafeId
), the current implementation fails to correctly consider thesafeId
parameter. Consequently, roles are assigned broadly to users without associating them specifically with asafeId
, allowing users with root access to any organization to modify roles for any user across all organizations.
Scenarios illustrating the impact:
- An organization (
uniOrg
) assigns a role to user A. A malicious attacker can create another organization (XOrg
) and use itssafeId
to either revoke the role from user A or grant different roles, essentially hijacking control over user privileges. - If a user with a role (e.g.,
SAFE_LEAD_MODIFY_OWNERS_ONLY
) in one organization leads a safe, they could create a new organization and assign themselves a different role (e.g.,SAFE_LEAD_EXEC_ON_BEHALF_ONLY
) in that new context.
The main impacts include:
- Malicious users could unduly assign or revoke roles, leading to unauthorized transaction executions.
- The revocation of roles can cause service denial for other organizations.
A proof of concept (PoC) demonstrates the vulnerability through specific test scenarios, where roots of different organizations can freely change roles of users, showing the exploitability of this flaw.
To mitigate this issue, roles should be assigned to users considering both safeId
and user
together, ensuring roles are specific to a safeId
.
Link: Issue #70
-
Unauthorized Access When Root Safe Joins New Organization Retaining Root Role
When a root safe, designated with the highest access level, exits an organization and joins a new one, it retains its root role. This misleads the contract to incorrectly recognize the safe as the root of the new organization, posing a significant risk by granting unauthorized access. The identified code in the
_createOrgOrRoot
function attributes this role to the root safe:
/// Assign SUPER_SAFE Role + SAFE_ROOT Role
RolesAuthority _authority = RolesAuthority(rolesAuthority);
_authority.setUserRole(
newRootSafe, uint8(DataTypes.Role.ROOT_SAFE), true
);
To illustrate the scenario:
- Safe A establishes an organization and becomes its root.
- Safe A exits this organization.
- Safe A joins a new organization as a member.
- The contract erroneously considers Safe A as the root in this new organization because it retained the
ROOT_SAFE
role from the previous organization.
The solution involves modifying the addSafe
function to revoke any existing ROOT_SAFE
role before adding the safe to a new organization. The proposed code amendment includes:
@@ -379,6 +379,14 @@ contract PalmeraModule is Auth, Helpers {
indexSafe[org].push(safeId);
/// Give Role SuperSafe
RolesAuthority _authority = RolesAuthority(rolesAuthority);
+ if (_authority.doesUserHaveRole(
+ newSafe.safe, uint8(DataTypes.Role.ROOT_SAFE))
+ ) {
+ _authority.setUserRole(
+ newSafe.safe, uint8(DataTypes.Role.ROOT_SAFE), false
+ );
+ }
if (
(
This change ensures that the root role is revoked when the safe is added to a new organization, preventing any unauthorized access. However, there's a contention that the root role is automatically removed upon exiting an organization, making this issue invalid. Additional verification is suggested to confirm this behavior.
Link: Issue #76
-
Exploit in registerOrg Function Allows Denial-of-Service and Gas Griefing Attacks
The
registerOrg
function in a contract is vulnerable to denial-of-service (DoS) and gas griefing attacks. This scenario occurs when a malicious user front-runs a legitimate user's transaction to register an organization name. The attacker's preemptive registration of the desired name causes the legitimate user's transaction to fail, forcing them to retry with a different name. This can be repeated, continuously hindering the user's efforts. The root cause is the function's failure to adequately check for existing names before allowing new registrations. To mitigate this, it is suggested to include the caller’s address in the hash when creating an organization, ensuring name uniqueness per user and preventing such attacks.Link: Issue #3
-
Hardcoded Depth Tree Limit Overrides Updates in createRootSafe Function
In the
PalmeraModule.sol
contract, there is a functionupdateDepthTreeLimit
for updating the depth tree limit, ensuring thedepthTreeLimit[org]
is set to thenewLimit
. However, there's a hardcoded depth tree limit of 8 in multiple places within the contract, such as in thecreateRootSafe
function, which sets the depth tree limit to 8 by default. This hardcoding means any changes made throughupdateDepthTreeLimit
won’t be reflected in these parts of the contract. The recommendation is to remove this hardcoded default to ensure the depth tree limit can be updated dynamically across the entire contract.Link: Issue #4
-
Denial of Service Vulnerability in Critical Organization Functions Due to Unbonded orgHash
Unbonded
orgHash
can potentially cause a denial of service (DOS) in core functions of a contract, includinggetOrgBySafe
,removeOrg
, andgetOrgHashBySafe
. An attacker can exploit this by creating numerous organizations, overloading the contract with unbondedorgHash
values, thus preventing legitimate users from interacting with the contract. This could lead to serious disruptions in core functionalities such as removing organizations or retrieving organization details, ultimately risking the contract's integrity and usability. Discussions suggest implementing a whitelisting mechanism to mitigate this risk, although the potential impact of such an attack remains significant, given that core functions' availability and interaction could be compromised.Link: Issue #10
-
Custom Contract Can Bypass isSafe() Checks and Access PalmeraModule Functions
A potential security loophole allows a user to create a custom contract that emulates a Safe Smart Account Wallet by returning
1
ongetThreshold()
calls. This enables the contract to utilize thePalmeraModule
functionality and bypassisSafe()
restrictions. Such non-safe contracts can register organizations, create root safes, and be added as safes inaddSafe()
. Consequently, these contracts can execute unauthorized actions likeexecTransactionOnBehalf()
, manipulate ownership withaddOwnerWithThreshold()
andremoveOwner()
, or perform arbitrary executions that could disrupt secure operations. It is recommended to implement stricter checks, such as usingERC165
supportsInterface()
, to ensure only true safe contracts pass theisSafe()
function.Link: Issue #24
-
Residual states retained after organization deletion can affect new registrations.
When organizations are deleted, not all related state information is properly removed. This can impact new users registering organizations with the same name, causing them to inherit residual states such as
allowFeature
,listed[org]
, andlistCount
. For example, if User A registers an organization "xyz" and sets up certain features, then deletes it, a new User B registering a new organization with the same name "xyz" will inherit these unwanted settings. This can lead to functional and security issues, forcing the new organization to take additional steps to reset these states, thus consuming more resources and effort. Clearing all related storage variables inremoveOrg
upon deletion is suggested to mitigate this problem.Link: Issue #27
-
Issue with setRole Function: Failing to Check
enabled
Parameter for Safe LeadsThe
setRole
function currently assigns the_safe.lead
attribute to a user if their role is related to leadership (e.g.,SAFE_LEAD
,SAFE_LEAD_EXEC_ON_BEHALF_ONLY
,SAFE_LEAD_MODIFY_OWNERS_ONLY
). However, it neglects to check whether theenabled
boolean parameter is true before updating_safe.lead
. This oversight can result in unauthorized role assignments, where users might be incorrectly designated as safe leads even if their role should be disabled. To mitigate this, the function should validate theenabled
parameter before assignment, and ifenabled
is false, ensure that_safe.lead
is revoked by setting it toaddress(0)
if the user holds no other lead roles. The proposed solution includes these safety checks to maintain proper access control.Link: Issue #41
-
Unauthorized
createRootSafe
Calls Allow Denial of Service on Safe ComposabilityThe current implementation allows the creation of a rootSafe for an organization without verifying if the provided
newRootSafe
is related or authorized. This vulnerabilty can be exploited to disrupt the safe orchestration in the palmera module. An attacker can use this flaw to perform a Denial-of-Service (DoS) by registering safes from other organizations under their own malicious organization, preventing these safes from functioning correctly within their intended hierarchies. For example, a malicious actor could front-run anaddSafe
transaction and register the safe under their malicious organization, causing the legitimate transaction to revert. A proposed solution involves adding an extra verification step to ensure thatnewRootSafe
has agreed to become the root safe for the given organization.Link: Issue #50
-
DoS Potential with Large Number of Safe Additions in Palmera Module
The Palmera module allows organizations to manage safes in a hierarchical structure, where a super safe can remove its child safes. However, any safe can arbitrarily become a child of any super safe by using the
addSafe
function, which increments thesuperSafeOrgSafe.child
array. This array has no limit on the number of child safes, only on hierarchy depth. When removing a safe, the system must update storage extensively by finding and updating thesafeId
in the child array and modifying child safes of the removed safe. This extensive process could lead to a Denial of Service (DoS) due to an Out of Gas (OOG) error if an excessive number of safes is involved, compromising the organization's safety. The proposed solution includes implementing limits on thesafe.child
array to prevent such scenarios.Link: Issue #51
-
addSafe Function Fails to Check for Removed superSafeId Causing Potential Issues
The
addSafe
function lacks a crucial check to ensure the providedsuperSafeId
is not in a removed state, which can lead to logical inconsistencies and potential security issues. Specifically, a removed safe ID could be incorrectly set as asuperSafeId
, compromising the integrity of the hierarchical structure of safes. Safes that have been removed should not be allowed to becomesuperSafe
again. To mitigate this, a validation step needs to be added to confirm that thesuperSafeId
is not in a removed state before adding a new safe. Although the initial debate considered this a non-issue, it was later confirmed as valid, highlighting a gap in theaddSafe
andupdateSuper
functions regarding the verification of safe states.Link: Issue #52
-
Denial of Service Vulnerability in
_exitSafe
Function When User Disables ModuleThe
_exitSafe
function currently contains a check forgetPreviewModule(caller)
that reverts if it returnsaddress(0)
. This logic can cause a Denial of Service (DoS) condition. Specifically, if users calldisableModule
on their own, subsequent calls to_exitSafe
will fail. This vulnerability impacts theremoveWholeTree
anddisconnectSafe
functions. IfdisableModule
has been invoked by the user,_exitSafe
will revert, preventing the proper execution of these functions and potentially leading to Protocol insolvency. A suggested mitigation is updating_exitSafe
to proceed even ifprevModule
isaddress(0)
by bypassing the reversion condition. This approach ensures the function does not unnecessarily fail and that_exitSafe
can execute properly even if the module has been deactivated.Link: Issue #55
-
Missing Validation for Guard and Module in addSafe Function Leads to Vulnerabilities
The
addSafe
function currently lacks validation to check if themsg.sender
has enabled the guard and thePalmeraModule
, which can lead to security concerns. Specifically, this allows safes to be added to an organization without any guard, paving the way for possible unauthorized access and Denial of Service (DoS) vulnerabilities indisconnectSafe
andremoveWholeTree
functions. A proof-of-concept demonstrates that a safe without an enabled guard and module was successfully added. To address this, a validation check should be added to theaddSafe
function to ensure themsg.sender
has enabled both the guard and the module. This oversight impacts other functions as well and needs comprehensive review.Link: Issue #57
-
Calling
disableSafeLeadRoles
Not Required for Root Safe inremoveWholeTree
FunctionIn the
removeWholeTree
function, thedisableSafeLeadRoles
function is not called for the root safe. The relevant code showsdisableSafeLeadRoles
being implemented for safes but not for the root safe, potentially causing issues if the root safe becomes a member of another organization. This omission can lead to unexpected behavior and security risks, as the root safe retains roles that should be disabled. To mitigate this, it is recommended to ensure thatdisableSafeLeadRoles
is also called for the root safe to prevent any associated issues. However, a counterpoint is presented claiming the root safe is removed correctly through the_exitSafe(rootSafe)
function, rendering the concern invalid. Verification and further deliberation are requested.Link: Issue #72
-
Issue with
getPreviewModule()
Function Returning Incorrect Next Pointer in Safe Contracts Version 1.3.0The
getPreviewModule()
function inHelpers.sol
has a bug when callingsafe.getModulesPaginated
. In Safe contracts version 1.3.0, used by Palmera, the function returns an incorrect next pointer, producing erroneous data. Although this issue is fixed in newer versions, Palmera remains on version 1.3.0. A related impact is found in theexecTransactionOnBehalf
function, which callscheckSignatures
andcheckNSignatures
. Safe version 1.3.0’scheckNSignatures
has a bug, now fixed in the latest versions. Despite the preference to use 1.3.0 due to its widespread adoption, on-chain tests with version 1.4.1 and the ERC-4337 module have passed successfully. Upgrading to a recent version of Safe is recommended.Link: Issue #78
-
Incorrect Calculation of PALMERA_TX_TYPEHASH in Constants Contract
The constant
PALMERA_TX_TYPEHASH
in the Constants contract is incorrectly calculated, leading to a breach of EIP-721 standards. The correct keccak256 hash should be0x33d86b91ace2c23c833e6a968f94ce2cdabd89ed7375f3d2135aa0f5a9c131b5
instead of the current value.Link: Issue #1
-
Incorrect Encoding of
bytes
Data and Signature Violates EIP-712 increateDigestExecTx
The
createDigestExecTx
function is incorrectly encodingbytes
data and signatures without using keccak256 hashing, violating EIP-712 standards. This results in incorrect encoding. The recommended fix is to keccak256 hash thebytes
arguments before computing the digest.Link: Issue #6
-
Enum.Operation Type Causes EIP-712 Hash Calculation Error
Enum.Operation is not a common type and cannot be used in EIP-712 hash calculations. Enums are derived from uint, so uint should be used instead. Using Enum.Operation results in an incorrect hash. A provided proof of concept demonstrates this issue.
Link: Issue #11
-
Deprecation of the
this
Keyword in Solidity 0.8.23 indomainSeparator
FunctionThe
domainSeparator()
function inHelpers.sol
uses the deprecatedthis
keyword, which can cause unexpected behavior in Solidity version 0.8.23. It is recommended to useaddress(this)
instead to adhere to best practices and avoid issues with deprecated variables and functions.Link: Issue #28
-
Access Control Issue with
removeSafe()
Function in Palmera Documentation and ImplementationThe current implementation of the
removeSafe()
function allows anySafeRegistered
user to call it, contrary to the documentation that stipulates only the root safe should. This risks unauthorized access to critical functionality. It is recommended to modify the access control to ensure only root safes can call this function, adhering strictly to the protocol’s intended design.Link: Issue #40
-
Vulnerability in PalmeraModule Contract Allows Malicious Delegatecall to Destroy targetSafe Contract
The
execTransactionOnBehalf
function in thePalmeraModule
contract lets specific roles like Safe Lead, Super Safe, and Root Safe execute transactions. A vulnerability arises if a malicious contract is targeted usingdelegatecall
, allowing it to self-destruct and destroy the targetSafe contract, disrupting the organization.Link: Issue #61
-
Parent Child Array Not Updated After Promoting Safe to Root
safeRoot's
promoteRoot()
function promotes a safe to Root but fails to update the parent safe's child array, causing data inconsistency. The parent array should be updated to remove the promoted safe, preventing potential vulnerabilities. A proposed code revision includes logic to remove the promoted child from the parent's child array efficiently.Link: Issue #89
-
Super Safe Role Not Revoked When Root Safe Promoted and Removed
When a safe is removed from an organization, its roles should be revoked. However, if a super safe is promoted to root, the super safe role isn't revoked, posing a risk. The problem can be demonstrated through scenarios where safes retain super safe roles even after org exits. To resolve this, super safe roles should be revoked before the exit.
Link: Issue #92
The audit of Palmera's competition on Hats.finance highlighted numerous security vulnerabilities within the PalmeraModule contract. Major concerns included insufficient access control in the execTransactionOnBehalf function, failures in role revocation, and improper role verification which could lead to unauthorized access and operations. Medium severity issues emphasized the need for robust organization and user state management; instances like denial-of-service attacks, mismanagement of safe hierarchies, and critical function failures were identified. Low severity issues included improper calculations and encoding that violate industry standards like EIP-712, and deprecated code usage that could lead to unexpected behavior. The audit's recommendations included comprehensive role-specific validation, ensuring proper state cleanups upon organization changes, and adhering to standard coding practices to mitigate these vulnerabilities. By addressing these findings, Hats.finance can enhance Palmera’s security posture significantly, ensuring safer and more robust operations in a decentralized ecosystem.
This report does not assert that the audited contracts are completely secure. Continuous review and comprehensive testing are advised before deploying critical smart contracts.
The Palmera audit competition illustrates the collaborative effort in identifying and rectifying potential vulnerabilities, enhancing the overall security and functionality of the platform.
Hats.finance does not provide any guarantee or warranty regarding the security of this project. Smart contract software should be used at the sole risk and responsibility of users.