The Llama governance system provides a unique way for protocol to leverage policies (represented by a non-transferable NFT) to permission action creation till execution. It primarily focuses on 2 mechanisms, Action creation and policy management. To summarize the protocol, here is a step-by-step flow:
- Protocol owners give policy and set roles (via
_setRoleHolder()
) - Protocol owner set permissions (via
_setRolePermissions()
) - Permissioned policy holders can create actions (via
createAction/createActionBySig
) - Strategy and custom guards validate action creation, if passes action can be queued (via Strategy and Guard function
validateActionCreation()
) - Policy holders with approval/disapproval cast votes during approval period (via
castApproval()/castDisapproval()
) - Strategies validate approval/disapproval against minimum thresholds
via isActionApproved()/isActionDisapproved()
- If approved and meets minimum execution time and action is not expired, action can now be executed, if not action is canceled
The following architecture improvements and feedback could be considered:
Could consider incorporating payment of action execution with common ERC-20 tokens (USDC, USDT, BNB ...). The tokens incorporated can be whitelisted to prevent ERC-20 tokens with other caveats from interacting with protocol until support is implemented (e.g. rebasing, fee-on-transfer)
Could consider creating a new type of Llama strategy in which approval/disapproval thresholds are specified as percentages of total supply and action creators are not allowed to cast approvals or disapprovals on their own actions for more flexibility
Checkpoint contracts seems to be deprecated by OpenZeppelin, not sure how this affects Llama contracts but since it affects core parts of the contract logic such as retrieving quantity
and expiration
data of roles, it might be worth noting.
Consider changing logic for action creation and checks for role before action creation. Protocol owners cannot set role with 0 quantity coupled with an expiration due to checks in _assertValidRoleHolderUpdate()
.
Only the approvalRole
is required to have quantity. All other roles that do not have approval power but have quantity
assigned to them will only incur unecessary computation.
Based on current implementation of _setRoleHolder
, protocol owners can never give a policyholder a role with an expiry with no quantity that represents approval/disapproval casting power. In the event where protocol owner wants to give policyholder a role that has 0 quantity
of votes, they can never do so.
Furthermore, hasPermissionId()
also checks for quantity before allowing creation of actions. This means policyholders can only create actions if some quantity
of approval/disapproval votes is assigned to them. Sementically, I don't think the quantity used for voting has relation to action creation.
Although that particular policy holder cannot vote unless approvalRole /disapprovalRole
is assigned to them, it can cause confusion where policy holders might think they can vote since some quantity
is assigned to them.
The following adjustments can be made:
- You could consider adding a third case in
_assertValidRoleHolderUpdate()
such as the following:
case3 = quantity == 0 && expiration > block.timestamp;
- Remove
quantity > 0
check inLlamaPolicy.hasPermissionId()
to allow action creators to create roles even when no quantity is assigned to them, since permissions to create actions are required to be set for policy holders viasetRolePermissions()
anyway. hasRole()
can simply check for expiration to determine if policy holder has role- A separate
hasCastRole()
can be created to specifically check for approval/disapproval Role
This way, quantity
will only ever need to be assigned to policyholders assigned with the approval/disapproval role.
In the policy-management.md
doc it states that
When roles are created, a description is provided. This description serves as the plaintext mapping from description to role ID, and provides semantic meaning to an otherwise meaningless unsigned integer.
However, there is no actual way to access roleId via role descriptions in contract. Policy holders cannot access role descriptions and roleIds convieniently except via protocol UI.
Hence, protocol could consider adding a new mapping to map roleIds to description and add logic to return role description and Id in LlamaPolicy.updateRoleDescriptions()
.
Since Id 0 is reserved for the bootstrap ALL_HOLDERS_ROLE
, the protocol owner could infact only have 254 unique roles.
So it may be good to consider using uint16
to allow 65534 unique roles.
Policy holders will approval/disapproval role and quantity of type(uint128).max
can force approval/disapproval of actions via forceApprovalRole/forceDisapproval
mapping.
Protocol owners can revoke policyholders anytime via LlamaPolicy.revokePolicy()
and prevent action creation/queuing/execution and approval/disapproval. It should be noted that as long as action is created, that action can be executed regardless policyholder is revoked or not, unless action is explicitly cancelled or disapproved.
The type of guards can be customized by protocol owners, so at any point of time specific guards can be set for specific action based on data input (selector) and possibly unfairly prevent execution of action via LlamaCore.setGuard()
.
A total of 4 days were spent to cover this audit, broken down into the following:
- 1st Day: Understand protocol docs, action creation flow and policy management
- 2nd Day: Focus on linking docs logic to
LLamaCore.sol
andLlamaPolicy.sol
, coupled with typing reports for vulnerabilities found - 3rd Day: Focus on different types of strategies contract coupled with typing reports for vulnerabilities found
- 4th Day: Sum up audit by completing QA report and Analysis