Event Horizon Arbitrum Franchiser Audit

Event Horizon Arbitrum Franchiser Audit

July 18, 2024

Summary

Type: Governance

Timeline: From 2024-07-01 To 2024-07-05

Languages: Solidity

Total Issues: 6 (1 resolved)

Notes & Additional Information: 6 (1 resolved)

Scope

We audited the HVAX/arbfranchiser repository at commit 8caba3d.

In scope were the following files:

src
├── FranchiserFactory.sol
├── Franchiser.sol
├── FranchiserLens.sol
├── base
│   └── FranchiserImmutableState.sol
└── interfaces
    ├── IVotingToken.sol
    ├── IFranchiserLens.sol
    ├── IFranchiserImmutableState.sol
    ├── Franchiser
    │   ├── IFranchiser.sol
    │   ├── IFranchiserErrors.sol
    │   └── IFranchiserEvents.sol
    └── FranchiserFactory
        ├── IFranchiserFactory.sol
        └── IFranchiserFactoryErrors.sol

The audit only reviewed the smart contract components intended to be deployed on-chain. Any deployment scripts, configurations, mock contracts, or tests were not reviewed.

System Overview

A recent discussion about delegating more voting power to retail investors ended in the passing of a snapshot vote which aims to delegate $ARB 7,000,000 to eventhorizoncommunity.eth. This is an EOA that the Event Horizon (EH) organization will use to represent the voters from their platform. ARB users can only delegate their entire voting power to another address. So, in order to delegate a specific amount, a contract has been created that will have the amount transferred to it and then delegate its voting power to EH.

The contract that EH provided to do this is a fork of a contract that was developed for Uniswap. It was privately audited by Trail of Bits and has a lot of functionality beyond the minimum needs of the ARB DAO. The top-level contract which the DAO will interact with is the FranchiserFactory contract. It is built to allow anyone to delegate their ERC-20 voting token, like ARB, to a delegatee. It does this by creating a Franchiser contract underneath that would mark the FranchiserFactory as the owner, hold the $ARB tokens from the caller, and call delegate on the token to transfer the voting power to the specified user (e.g., the EH EOA).

When the DAO is ready to take back the funds, a call to recall will pull the tokens out of the Franchiser and return them. There are added methods to batch delegation and recalling which we do not anticipate the DAO will need in this specific case but could use if more delegations of this sort arise. The FranchiserFactory contract will create a Franchiser contract as needed for each unique delegator/delegatee pair, while each Franchiser is set up as a minimal proxy of a Franchiser implementation contract.

Each Franchiser contract has to ability for the delegatee to subdelegate their delegated tokens to further subdelegatees. This creates a tree of Franchise contracts with the original delegator/delegatee Franchise contract at the head, each subdelegation from a delegatee at each child node, and the tokens being transferred down the tree to imbue voting power. The original delegatee can sub-delegate up to eight times with each of these sub-delegating up to four, then two, then one. Counting the maximum possible nodes in each level of the tree totals to $(1)+(8)+(8 \cdot 4)+(8 \cdot 4 \cdot 2)+(8 \cdot 4 \cdot 2) = 169$ nodes. At each Franchiser contract in this tree, the delegatee can unsubdelegate and return all of the funds transferred down the tree to their own contract and restore their full voting power delegation. The recall method mentioned above does the same thing but returns all tokens from the tree to the original delegator.

The scope also includes an optional FranchiserLens contract which contains view functions to get data from the Franchiser tree in flat formats. It is not necessary for the other contracts in scope but would give the DAO easy visibility into their delegation and any further subdeldegations.

Security Model and Trust Assumptions

This code relies on the OpenZeppelin Contracts library for cloning, checking for code size, and for set operations. It also relies on the Solmate library for ownership and transferring tokens. We assume these libraries work as described and intended and both have long on-chain history.

Notes & Additional Information

Unused Import

The import import {Franchiser} from "../../Franchiser.sol"; imports unused alias Franchiser in IFranchiserEvents.sol.

Consider removing or using any unused imports to improve the overall clarity and readability of the codebase.

Update: Acknowledged with comment “we will leave the stylistic notes as is as there are no objections to functionality.”

Unused Named Return Variable

Named return variables are a way to declare variables that are meant to be used within a function’s body for the purpose of being returned as that function’s output. They are an alternative to explicit in-line return statements.

In FranchiserLens.sol, the delegation return variable for the getRootDelegation function is unused.

Consider either using or removing any unused named return variables.

Update: Acknowledged with comment “we will leave the stylistic notes as is as there are no objections to functionality.”

Lack of Events

The FranchiserFactory contract does not any emit events when new delegations are made or recalled.

Consider emitting events in functions that change the state of delegations.

Update: Acknowledged with comment “we will leave the stylistic notes as is as there are no objections to functionality.”

Inconsistent Access Control

In the Franchiser contract, subDelegate, subDelegateMany, unSubDelegate, and unSubDelegateMany will revert if called by anyone who is not the delegatee of the contract. However, subDelegateMany is the only function that is not marked as such and will revert upon an inner call to subDelegate.

To improve code clarity, consider marking subDelegateMany with the onlyDelegatee modifier like the other delegatee-restricted functions.

Update: Acknowledged with comment “we will leave the stylistic notes as is as there are no objections to functionality.”

initialize Specificity

The Franchiser contract has two functions called initialize. The first one has three parameters and is only directly called from the FranchiserFactory contract to create a top-level delegation of tokens. The second function wraps the first, has two parameters, and is only used to create subdelegations of tokens from existing Franchiser contracts.

To improve code clarity and readability, consider naming the second initialize function more specifically (e.g., initializeSubFranchise).

Update: Acknowledged with comment “we will leave the stylistic notes as is as there are no objections to functionality.”

Maximum Subdelegatees

The code has been designed for generic delegation and subdelegation of tokens. If the FranchiserFactory contract is to be used solely for this one EH delegation, it is possible to change the INITIAL_MAXIMUM_SUBDELEGATEES to one and make further subdelegations by EH impossible. This would ensure that they do not have the ability to further subdelegate to another actor who is not in the proposal but would make vote splitting by subdelegation impossible by EH (at least until fractional voting is brought to the governor).

Of course, by delegating in the first place, the DAO is already trusting EH to behave as promised. Thus, further subdelegations being used maliciously to vote in other ways appears to be a minor risk that is mitigated by the ability to revoke the funds. However, revoking is a slow process that would require a vote to pass as the L2 Governor would be the original delegator. A way around it would be to transfer the funds directly to the security council and entrust them to create the subdelegation from the FranchiserFactory contract. We are not aware, however, of a precedent or authority for the council to behave this way.

Possible ways to improve the code and tailor it specifically to Arbitrum’s needs would be to give the non-emergency security council the ability to recall the funds. This can be done by allowing the council to be specified at initial delegation and adding an ability for them to call the recall function. However, even then, a call to recall (even unSubDelegate) could be front-run to get in a last vote before losing voting power. A fix for this could be to add some sort of authorization method for the delegatee to vote only on approved votes, but this seems overly protective and operationally taxing to implement. No matter what the community chooses or how the contract iterates, we will review any on-chain proposal for construction/delegation and opine on its safety and features.

Update: Resolved in commit 101d01d.

Conclusion

This audit covered the Franchiser contracts provided by Event Horizon. They will allow the Arbitrum DAO to transfer a token through the FranchiserFactory to a Franchiser contract which will delegate it’s voting power to Event Horizon, allowing them to vote in proposals. The Franchiser contract as audited allows Event Horizon to further subdelegate their tokens. If the DAO is not interested in using this contract for further grants, we recommend the DAO remove this subdelegation ability from the contracts by setting the INITIAL_MAXIMUM_SUBDELEGATEES constant to one. We also found no security issues with the contracts and our notes contain only recommendations for coding practices. We welcome iteration on the contracts and will be auditing any changes and on-chain proposals involving them. We are grateful to the Event Horizon team for being responsive during the audit and for bringing a compelling use case to the DAO.