Skip to content

Ownership in Curve Contracts

Smart contracts often have owner-guarded functions which only allow specific addresses to call certain functions. These are typically used for administrative operations like modifying liquidity pool parameters, updating fee receivers, or changing critical contract settings.

Ownership Agents

To make ownership work along with the DAO governance, Curve makes use of OwnershipAgents on Ethereum, aswell as on other L1 or L2s to handle governance actions.

The main OwnershipAgent is deployed at 0x40907540d8a6C65c637785e8f8B742ae6b0b9968 on Ethereum. Deployments on other chains can be found here.

Curve strives to remain as decentralized as possible, with most administrative operations controlled by the DAO. However, there are some exceptions where DAO control may not be optimal:

Contracts like the AddressProvider or MetaRegistry that:

  • Don't hold any user assets
  • Only provide on-chain information
  • Require frequent maintenance and quick updates

In these specific cases, ownership may be delegated to specialized administrative addresses.


Ownership Transfer Patterns

Curve uses different ownership transfer patterns in its contracts. The most common ones are described below.

commit + accept

Source code

The implementation of the commit + accept pattern might vary slightly. Nontheless, the general idea is the same.

owner: public(address)
future_owner: public(address)

event TransferOwnership:
    _old_owner: address
    _new_owner: address

@external
def commit_transfer_ownership(_future_owner: address):
    """
    @notice Transfer ownership to `_future_owner`
    @param _future_owner The account to commit as the future owner
    """
    assert msg.sender == self.owner  # dev: only owner

    self.future_owner = _future_owner


@external
def accept_transfer_ownership():
    """
    @notice Accept the transfer of ownership
    @dev Only the committed future owner can call this function
    """
    assert msg.sender == self.future_owner  # dev: only future owner

    log TransferOwnership(self.owner, msg.sender)
    self.owner = msg.sender

The ownership transfer mechanism implements a secure two-step process that prevents accidental or malicious ownership transfers. At its core, the implementation revolves around two state variables: owner and future_owner, both public addresses that track the current and prospective contract owners respectively.

The transfer process begins when the current owner initiates a transfer by calling commit_transfer_ownership. This function takes a single parameter - the address of the intended new owner - and stores it in the future_owner state variable. Importantly, this function can only be called by the current owner, enforced through an assertion check at the start of the function.

Once the transfer is committed, the second phase of the transfer can begin. The designated future_owner must actively accept the ownership by calling accept_transfer_ownership. This function performs its own security check, ensuring that only the committed future_owner can call it. Upon successful execution, it updates the owner state variable to the new address and emits a TransferOwnership event that logs both the old and new owner addresses.

This two-step process provides several security benefits. First, it prevents ownership transfers due to accidental input of wrong addresses, as the intended recipient must actively accept the role. Second, it ensures that the new owner has control of their address and can actually interact with the contract before the transfer is complete. The process also leaves a clear on-chain trail through the emitted event, making ownership transfers transparent and auditable.

Google Colab Notebook

A simple Google Colab notebook that simulates the commit + accept pattern can be found here: Google Colab Notebook.


commit + apply

Source code

The implementation of the commit + apply pattern might vary slightly. Nontheless, the general idea is the same.

event CommitOwnership:
    admin: address

event ApplyOwnership:
    admin: address

admin: public(address)
future_admin: public(address)

@external
def commit_transfer_ownership(addr: address):
    """
    @notice Transfer ownership of GaugeController to `addr`
    @param addr Address to have ownership transferred to
    """
    assert msg.sender == self.admin  # dev: admin only
    self.future_admin = addr
    log CommitOwnership(addr)


@external
def apply_transfer_ownership():
    """
    @notice Apply pending ownership transfer
    """
    assert msg.sender == self.admin  # dev: admin only
    _admin: address = self.future_admin
    assert _admin != ZERO_ADDRESS  # dev: admin not set
    self.admin = _admin
    log ApplyOwnership(_admin)

This implementation presents another variation of the two-step ownership transfer pattern, commonly used in Curve's contracts, particularly in the GaugeController. Instead of requiring the future owner to accept the transfer, this pattern allows the current admin to both initiate and complete the transfer process.

The mechanism uses two state variables: admin (instead of owner) and future_admin, following the same principle of separating the current and prospective contract administrators. The process is tracked through two distinct events: CommitOwnership and ApplyOwnership, providing clear on-chain visibility of the transfer stages.

The transfer process begins with the current admin calling commit_transfer_ownership, specifying the address of the intended new administrator. This function sets the future_admin state variable and emits a CommitOwnership event. Unlike the accept pattern, this function includes a crucial security check ensuring that only the current admin can initiate the transfer.

The second phase involves calling apply_transfer_ownership, which finalizes the transfer. This function includes several important security features: - Only the current admin can execute the transfer - The function verifies that future_admin is not set to the zero address - Upon successful execution, it updates the admin state and emits an ApplyOwnership event

This implementation differs from the accept pattern in a key aspect: the current admin maintains full control throughout the entire process, rather than requiring action from the future admin. While this provides more flexibility for the current admin, it also means extra care must be taken to ensure the new admin address is correct and capable of managing the contract.

The zero-address check in the apply function serves as an additional safety mechanism, preventing transfers to invalid addresses that could permanently lock the contract's administrative functions.

Google Colab Notebook

A simple Google Colab notebook that simulates the commit + apply pattern can be found here: Google Colab Notebook.


snekmate

sneks coming soon 🐍🐍🐍