[BIP-140] Introduce TimelockAuthorizer

Introduction

Balancer’s access control system is based on permissions, which can be granted and revoked. These are controlled by the Authorizer contract, which was deployed along with the Vault in April 2021.

This proposal aims to replace it with the TimelockAuthorizer, which provides more fine-grained control over permissions, as well as new security features. It additionally addresses a bug in the AuthorizerAdaptor contract that was disclosed via the Immunefi bug bounty program.

For simplicity, this migration will initially only take place on the main Ethereum network and none of the L2s or sidechains.

Specification

The TimelockAuthorizer contract (developed by Balancer Labs) is a drop-in replacement of the current Authorizer contract, enabling new use cases and providing security improvements.

I encourage everyone to read more about each of these improvements, as they will greatly impact the operation of the Balancer DAO if this proposal where to pass (particularly the ‘Delays’ section).

Granular Permissions

As of today, permissions are global: an account with a permission can use it on any contract in the entire network. For singleton contracts (like the Vault or the ProtocolFeeWithdrawer) this is not an issue since there is only one of them. However, for contracts with multiple instances (like liquidity pools) this means that an account with e.g. permission to set swap fees can use that permission on any pool of the same type (i.e. that is created in the same factory).

TimelockAuthorizer allows granting permissions over a single contract, meaning it will e.g. be possible to allow an account to manage a single pool’s swap fees while not being able to do the same on any other pool. This has always been supported on the original IAuthorizer interface - the current Authorizer simply did not implement this capability (for simplicity).

It will still be possible to grant global permissions by passing the special sentinel address known as EVERYWHERE (equal to 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF).

Granters and Revokers

The current Authorizer features a single all-powerful role (0x00..00), currently held by the Balancer DAO Governance Multisig (0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f). Accounts with this role can grant and revoke any permission - but they are the only one that can do so.

TimelockAuthorizer instead distinguishes between having a permission and being a granter or a revoker for a permission. This will increase the capacity of the the DAO Governance Multisig to delegate subtasks to other accounts. For example, instead of simply granting permission to trigger the Liquidity Mining BAL bridging process to the Balancer Labs Operations Multisig, it could make said multisig a granter, increasing flexibility (e.g. letting Balancer Labs develop contracts that trigger this process automatically and grant them permission to operate without having to involve the Governance Multisig).

The TimelockAuthorizer also has a root account, initially set to the Balancer DAO Governance Multisig, which always has permission to grant and revoke all permissions, and to create and destroy granters and revokers. These permissions cannot be revoked by anyone (except by transferring root to a different account), meaning it always retains full control over the system and can safely evict any rogue granters or revokers.

Delays

Arguably the most important feature of the TimelockAuthorizer is the capacity to assign minimum delays to certain actions. If an action has a delay, then accounts with that permission cannot immediately execute said action: they can only schedule an execution at some time in the future (greater than the action’s delay).

Critically, these scheduled executions can be cancelled, which lets the community detect and stop malicious or erroneous actions after they are committed on-chain but before they are actually executed, greatly decreasing the risk of user error, phishing attacks, etc. This also provides users who are not happy with a future action (e.g. increased fees) with the opportunity to exit the system before those measures take place.

It is also possible to assign delays to the granting and revoking of permissions. By e.g. delaying granting and revoking the permission to mint BAL (currently only held by the automated Liquidity Mining system), we end up with a much stronger committment to said system, and greatly reduce the risk of e.g. a malicious actor somehow acquiring minting capabilities. Similar measures can be applied to other powerful permissions, such as relayer privilege, adding gauge factories to the GaugeAdder, etc.

In general, if a multisig wields a permission (e.g. adding gauges) then its execution should have a delay (to review the multisig’s action). Instead, if a contract wields a permission (e.g. the GaugeAdder) then its granting should be delayed (to review the code of the contract that will use it).

For more information on how delays relate to security, see my talk at the DeFi Security Summit.

Authorizer Adaptor Entrypoint

A team of independent security researchers submitted a vulnerability disclosure via the Immunefi bug bounty program that relates to the operation of the AuthorizerAdaptor. Said issue was promptly mitigated and resolved, but operation of some components of the system (particularly adding reward tokens and killing gauges) has been degraded as a result. The TimelockAuthorizer includes a fix for this in the form of the AuthorizerAdaptorEntrypoint, which provides a secure way to restore regular operation.

Migration Procedure

To ease the transition to this new system, Balancer Labs has also developed the TimelockAuthorizerMigrator contract, which prepares the TimelockAuthorizer by granting some of the permissions that exist in the current Authorizer, as well as setting up some initial delays.

In order to complete the migration, the migrator needs permission to call setAuthorizer in the Vault, which can only be granted by the Balancer DAO Governance Multisig. Additionally, the Multisig must also claim the root role in the TimelockAuthorizer, completing the root transfer from the migrator to the Multisig. This cannot be done before January 5th, 2023, since the root transfer has a minimum delay of 30 days.

Execution Details

From the Balancer DAO Governance Multisig (0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f):

  • Call grantRole on the Authorizer (0xA331D84eC860Bf466b4CdCcFb4aC09a1B43F3aE6) with:
  • role: 0x1cbb503dcc0f4acaedf71a098211ff8b15a220fc26a6974a8d9deaab040fa6e0

  • account: 0xf8ee6f1F9B54F9b2C192D703ea2d22112cBC062b

  • Call claimRoot on the TimelockAuthorizer (0x9E3cD0606Db55ac68845bB60121847823712ae05)

The role argument corresponds to the permission to call setAuthorizer on the Vault, and account is the TimelockAuthorizerMigrator.

The TimelockAuthorizer address can also be found in its deployment.

Recall that claimRoot cannot be called until January 5th, 2023.

Once these two steps are done, any account in the network can call finalizeMigration on the TimelockAuthorizerMigrator (0xf8ee6f1F9B54F9b2C192D703ea2d22112cBC062b) to complete the process.

Initial Configuration

No new permissions will be granted. The Migrator contract ensures that all permissions granted during the migration exist in the current Authorizer. Some of the current permissions have been left out however, since they no longer apply (e.g. permission to pause the Vault, which can no longer be paused as of July 2021).

Any permissions granted after the deployment of the TimelockAuthorizer (Nov 30th) are not automatically migrated, and will have to be re-granted after the migration.

Initial Delays

The following actions have initial delays associated to them. As a rule of thumb, I propose that we assign delays to actions with high impact and that do not require urgent execution.

Execution

  • Vault.setAuthorizer, 30 days. All delays must be shorter or equal to this one, as they could be otherwise bypassed by simply changing the Authorizer and removing delays entirely.
  • SmartWalletChecker.allowlistAddress, 7 days. Once veBAL is locked this cannot be undone in any way, so extra care should be taken when adding contracts to the allowlist.
  • VotingEscrowDelegationProxy.setDelegation, 14 days. veBAL boost upgrades are infrequent and should be handled with care, since they can greatly affect the veBAL operation.
  • GaugeAdder.addGaugeFactory, 14 days. Adding new gauge factories is a critical action as it grants them permission to mint BAL. See my talk at ETHLatam (in Spanish) for more details.

Granting

  • BalancerTokenAdmin.mint, 30 days. This prevents other accounts from being able to mint BAL.
  • ProtocolFeesCollector.withdrawCollectedFees, 30 days. This prevents bypassing the ProtocolFeesWithdrawer.
  • GaugeController.addGauge, 14 days. This prevents bypassing the GaugeAadder.
  • BALTokenHolder.withdrawFunds, 7 days. This manages the BAL minted for the veBAL gauge.
  • Vault relayer permissions, 7 days. Relayers are quite powerful, so creating them should be carefully reviewed.

The creation of the full list of permissions and their delays can be inspected in this script.

Risk

The TimelockAuthorizer is a relatively complex contract, which will be used to manage all permissions on the Balancer ecosystem, including the Liquidity Mining program and management of Protocol Fees. As such, care should be taken to review it.

The Balancer Labs team has been working on this contract for around 8 months, and we are confident in its correctness. It has been audited by ABDK, and we’re currently working with Certora to conduct a second review.

7 Likes

sir, was this implemented?

thank you

1 Like

we’ll vote on it this week. veBAL voters don’t take holidays

4 Likes

Vote is pending: https://snapshot.org/#/balancer.eth/proposal/0x46b48f89247d28fc26a3851269ca30424b7cfb1dede75b99b77e6a27b998dd19

1 Like

@nventuro Might be a bit off-topic but didn’t know where else to ask. I noticed that fine-grained authorization is granted for approved relayers by the DAO, and that normal users then add those relayers as approved delegators if they want to use them.

One question I have - is there a reason for not giving further access control at the user level? E.g. user A granting permission for relayer B to call a specific function (e.g. addLiquidity), but only that function.

This would basically allow users to delegate certain functionality to contracts or SAFEs or even relayers, but with further permissions restrictions, similar to how when you approve an app to do a certain set of functionalities (e.g. grant Google access to read your contact list).

My overarching question here is why not extend the flexibility further so users themselves can restrict the permissions of their relayers?

1 Like

I don’t think we ever considered such a model, mostly because it would’ve made the code and UX more complicated for little benefit. Each user would need to select which actions they want to enable for their relayer, which would’ve meant tracking and checking each of those individually in the contract (on top of the global permissions!).

Rather the thinking was to design relayers in such a way that it wouldn’t be a security concern to approve the relayer, since it’d have the same security implications as regular Vault functions. In your example, there’s no notion of ‘approving’ the Safe to only call the Vault’s swap function but not joinPool. Similarly, once you approve relayers then all of their functionality is equally safe.

(the reason why you actually need to approve relayers is to prevent rogue actors or compromised keys from registering malicious relayers which would then be able to steal funds - because each user individually opts in such an actor would need to also trick users into performing this action).

1 Like

Thanks for shedding the insight @nventuro !

As I understand, the current design works where authorizers have granular permissions on what relayers can call what functions:

// actionId → account → where → isGranted
mapping(bytes32 => mapping(address => mapping(address => bool))) private _isPermissionGranted;

And then users can approve relayers through the following:

// user → relayer → isGranted
mapping(address => mapping(address => bool)) private _approvedRelayers;

What if I wanted to approve to, say, another user, a SAFE, or even a multisig, to perform a swap but ONLY a swap, on my behalf? Wouldn’t extending the second mapping to something like:

// user → actionId → relayer → isGranted
mapping(address => mapping(actionId => mapping(address => bool))) private _approvedRelayers;

Help enable this behavior? And by default users could simply approve the whole relayer / EOA / SAFE / user by just doing _approvedRelayers[user][*][relayer][true];

In which case, the first mapping focuses on core, approved relayers (for service to service communication), and the latter focuses on user approvals of relayers or any other external entity that may or may not have been granted access for internal protocol authorization.

Is the idea that the benefits of more granular authorization does not outweigh the UX friction this adds?

And if so, how can users currently delegate to other entities (EOAs / multisigs / relayers) to peform certain actions without protocol approval, if these are entities they trust?

Thanks!

1 Like