CurveLMCallbackFactory
The CurveLMCallbackFactory deploys LMCallback contracts for Llamalend markets from a single blueprint. An LM Callback acts as a liquidity gauge for the collateral deposited in a market's AMM: once it receives gauge weight in the GaugeController, it streams CRV emissions to the market's borrowers.
The factory only deploys callbacks and keeps a registry of them. Deploying a callback does not activate it: a callback starts earning only after it is set as the AMM's liquidity mining callback through the market's Configurator and added to the GaugeController by the DAO.
Deployment is permissionless. Anyone can call deploy_lm_callback for an AMM, while the owner of the factory controls the blueprint, can pause new deployments, and is the account allowed to pause CRV rewards on every callback deployed by this factory (see LMCallback.set_killed).
LMCallbackFactory.vyThe source code for the LMCallbackFactory.vy contract can be found on GitHub. The contract is written in Vyper version 0.4.3 and uses the Snekmate ownable and pausable modules.
The contract is deployed on Ethereum at
0x916E2B5E3Df268Ce2348265d2AAD4BB9f2B3dFA0.
{ }Contract ABI▼
[{"anonymous":false,"inputs":[{"indexed":true,"name":"previous_owner","type":"address"},{"indexed":true,"name":"new_owner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"old_blueprint","type":"address"},{"indexed":false,"name":"new_blueprint","type":"address"}],"name":"UpdateLMCallbackBlueprint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"amm","type":"address"},{"indexed":true,"name":"deployer","type":"address"},{"indexed":true,"name":"blueprint","type":"address"},{"indexed":false,"name":"lm_callback","type":"address"}],"name":"DeployedLMCallback","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"new_owner","type":"address"}],"name":"transfer_ownership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_amm","type":"address"}],"name":"deploy_lm_callback","outputs":[{"name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_i","type":"uint256"}],"name":"get_lm_callback","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"get_lm_callback_count","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_blueprint","type":"address"}],"name":"set_blueprint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lm_callback_blueprint","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"is_valid_gauge","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"get_lm_callback_by_amm","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"get_blueprint_by_lm_callback","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_owner","type":"address"},{"name":"_blueprint","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"}]
Deploying LM Callbacks
deploy_lm_callback
LMCallbackFactory.deploy_lm_callback(_amm: IAMM) -> addressDeploys a new LMCallback for the AMM _amm from the current lm_callback_blueprint. Anyone can call this function while the factory is not paused.
The new callback is registered as a valid gauge (is_valid_gauge), appended to the list of deployed callbacks, recorded as the newest callback for the AMM (get_lm_callback_by_amm), and linked to the blueprint it was created from (get_blueprint_by_lm_callback).
The call reverts with "already deployed" if the AMM's newest callback was created from the blueprint that is currently set, so a market cannot receive two identical callbacks back to back. Only the newest callback is compared: after the blueprint has been rotated, a new callback can be deployed for an AMM that already has one from an earlier blueprint.
The callback constructor reads the collateral token from _amm.coins(1) and requires it to have 18 decimals. The function is reentrancy-locked because the blueprint constructor calls into the caller-supplied AMM.
Deploying a callback does not attach it to the AMM. Attaching is done through the market's Configurator, and CRV only flows once the DAO has added the callback to the GaugeController.
| Input | Type | Description |
|---|---|---|
_amm | address | Llamalend AMM the LM Callback is deployed for. |
Returns: address of the deployed LM Callback (address).
Emits: DeployedLMCallback event.
<>Source code▼
- LMCallbackFactory.vy
- pausable.vy (Snekmate 🐍)
event DeployedLMCallback:
amm: indexed(address)
deployer: indexed(address)
blueprint: indexed(address)
lm_callback: address
MAX_LM_CALLBACKS: constant(uint256) = 10**18
lm_callback_blueprint: public(address)
is_valid_gauge: public(HashMap[address, bool])
get_lm_callback_by_amm: public(HashMap[address, address])
get_blueprint_by_lm_callback: public(HashMap[address, address])
_lm_callbacks: DynArray[address, MAX_LM_CALLBACKS]
@external
@nonreentrant
def deploy_lm_callback(_amm: IAMM) -> address:
"""
@notice Deploy an LM Callback
@dev Reentrancy-locked because the blueprint constructor hands control to
the caller-supplied `_amm`; the lock keeps the registry writes below
atomic with respect to the deployment they describe
@dev Reverts if the AMM's newest callback came from the blueprint currently
set, so a market cannot be handed two identical callbacks back to back.
Only that newest callback is compared, not every one ever deployed for the
AMM: rotating the blueprint away and back therefore does allow another
deployment from the earlier blueprint
@param _amm LlamaLend AMM the deployed LM Callback is going to be used for
@return Address of the deployed LM Callback
"""
pausable._require_not_paused()
lm_callback_blueprint: address = self.lm_callback_blueprint
existing_lm_callback: address = self.get_lm_callback_by_amm[_amm.address]
if existing_lm_callback != empty(address):
assert (
self.get_blueprint_by_lm_callback[existing_lm_callback]
!= lm_callback_blueprint
), "already deployed"
lm_callback: address = create_from_blueprint(
lm_callback_blueprint,
_amm,
code_offset=3,
)
self.is_valid_gauge[lm_callback] = True
self._lm_callbacks.append(lm_callback)
self.get_lm_callback_by_amm[_amm.address] = lm_callback
self.get_blueprint_by_lm_callback[lm_callback] = lm_callback_blueprint
log ILMCallbackFactory.DeployedLMCallback(
amm=_amm.address,
deployer=msg.sender,
blueprint=lm_callback_blueprint,
lm_callback=lm_callback,
)
return lm_callback
paused: public(bool)
@internal
def _require_not_paused():
"""
@dev Throws if the contract is paused.
"""
assert not self.paused, "pausable: contract is paused"
▶Example▼
>>> LMCallbackFactory.deploy_lm_callback("0xAMM...")
'0xLMCallback...'
Blueprint
lm_callback_blueprint
LMCallbackFactory.lm_callback_blueprint() -> address: viewGetter for the blueprint that new LM Callbacks are deployed from. It is set in the constructor and can be changed by the owner via set_blueprint. The blueprint can never be the zero address.
Returns: LM Callback blueprint (address).
<>Source code▼
lm_callback_blueprint: public(address)
@deploy
def __init__(
_owner: address,
_blueprint: address,
):
"""
@notice Factory which creates LM Callbacks for LlamaLend/crvUSD markets from a blueprint
@param _owner Owner of the factory, allowed to update the blueprint (ideally DAO)
@param _blueprint Address of the LM Callback blueprint
"""
ownable.__init__()
pausable.__init__()
assert _owner != empty(address) # dev: zero owner
ownable._transfer_ownership(_owner)
self._set_blueprint(_blueprint)
@internal
def _set_blueprint(_blueprint: address):
assert _blueprint != empty(address) # dev: zero blueprint
log ILMCallbackFactory.UpdateLMCallbackBlueprint(
old_blueprint=self.lm_callback_blueprint, new_blueprint=_blueprint
)
self.lm_callback_blueprint = _blueprint
▶Example▼
set_blueprint
LMCallbackFactory.set_blueprint(_blueprint: address)This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the factory, which is intended to be the Curve DAO. Changing the code that future LM Callbacks are deployed from is therefore in the hands of the owner.
Sets a new blueprint for future LM Callback deployments. Callbacks that were already deployed are not affected. The blueprint can be rotated but never unset: passing the zero address reverts, so new deployments are halted with pause instead.
| Input | Type | Description |
|---|---|---|
_blueprint | address | Address of the new LM Callback blueprint. |
Emits: UpdateLMCallbackBlueprint event.
<>Source code▼
- LMCallbackFactory.vy
- ownable.vy (Snekmate 🐍)
event UpdateLMCallbackBlueprint:
old_blueprint: address
new_blueprint: address
lm_callback_blueprint: public(address)
@external
def set_blueprint(_blueprint: address):
"""
@notice Set the blueprint
@dev Reverts on the empty address: the blueprint can be rotated but never
unset, so deployments are halted with pause() instead
@param _blueprint The address of the blueprint to use
"""
ownable._check_owner()
self._set_blueprint(_blueprint)
@internal
def _set_blueprint(_blueprint: address):
assert _blueprint != empty(address) # dev: zero blueprint
log ILMCallbackFactory.UpdateLMCallbackBlueprint(
old_blueprint=self.lm_callback_blueprint, new_blueprint=_blueprint
)
self.lm_callback_blueprint = _blueprint
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
▶Example▼
>>> LMCallbackFactory.set_blueprint("0xNewBlueprint...")
get_blueprint_by_lm_callback
LMCallbackFactory.get_blueprint_by_lm_callback(arg0: address) -> address: viewGetter for the blueprint an LM Callback was deployed from. It is used for the duplicate check in deploy_lm_callback and lets integrations tell callback generations apart. Returns the zero address for addresses not deployed by this factory.
| Input | Type | Description |
|---|---|---|
arg0 | address | Address of the LM Callback. |
Returns: blueprint the callback was created from (address).
<>Source code▼
# Blueprint each callback was created from, kept for the duplicate check in
# `deploy_lm_callback` and so integrations can tell callback generations apart
get_blueprint_by_lm_callback: public(HashMap[address, address])
▶Example▼
Querying LM Callbacks
get_lm_callback
LMCallbackFactory.get_lm_callback(_i: uint256) -> address: viewReturns the LM Callback deployed at index _i, in deployment order. Reverts if _i is not lower than get_lm_callback_count.
| Input | Type | Description |
|---|---|---|
_i | uint256 | Index of the LM Callback. |
Returns: LM Callback (address).
<>Source code▼
_lm_callbacks: DynArray[address, MAX_LM_CALLBACKS]
@external
@view
def get_lm_callback(_i: uint256) -> address:
"""
@notice Get the LM Callback deployed at index `_i`
@param _i Index of the LM Callback
@return Address of the LM Callback
"""
return self._lm_callbacks[_i]
▶Example▼
>>> LMCallbackFactory.get_lm_callback(0)
'0xLMCallback...'
get_lm_callback_count
LMCallbackFactory.get_lm_callback_count() -> uint256: viewReturns the number of LM Callbacks deployed by this factory.
Returns: number of deployed LM Callbacks (uint256).
<>Source code▼
_lm_callbacks: DynArray[address, MAX_LM_CALLBACKS]
@external
@view
def get_lm_callback_count() -> uint256:
"""
@notice Get the number of LM Callbacks deployed by this factory
@return Number of deployed LM Callbacks
"""
return len(self._lm_callbacks)
▶Example▼
get_lm_callback_by_amm
LMCallbackFactory.get_lm_callback_by_amm(arg0: address) -> address: viewGetter for the newest LM Callback this factory deployed for an AMM. This records deploy-time intent, not live state: the Configurator can attach any address as the callback, and a detach is never reflected here. To find the callback currently attached to a market, query liquidity_mining_callback() on the AMM itself.
| Input | Type | Description |
|---|---|---|
arg0 | address | Address of the AMM. |
Returns: newest LM Callback deployed for the AMM (address).
<>Source code▼
# The newest callback this factory deployed for an AMM - deploy-time intent, not
# live state: the Configurator can attach any address it likes, and a detach is
# never reflected here. Ask the AMM itself for what is currently attached
get_lm_callback_by_amm: public(HashMap[address, address])
▶Example▼
is_valid_gauge
LMCallbackFactory.is_valid_gauge(arg0: address) -> bool: viewChecks whether an address is an LM Callback deployed by this factory. The getter is named after the gauge factories' is_valid_gauge because integrations validate gauges by asking their factory, and LM Callbacks are gauges from their point of view.
A True result only means the callback was deployed by this factory. It says nothing about whether the callback is currently attached to its AMM or whether its rewards are paused; see LMCallback.is_killed for that.
| Input | Type | Description |
|---|---|---|
arg0 | address | Address to check. |
Returns: True if deployed by this factory (bool).
<>Source code▼
# Named after the gauge factories' getter rather than after the callback:
# integrations validate a gauge by asking its factory for `is_valid_gauge`,
# and LM Callbacks are gauges from their point of view
is_valid_gauge: public(HashMap[address, bool])
▶Example▼
Pausing Deployments
Pausing only stops new deployments through deploy_lm_callback. It has no effect on callbacks that are already deployed; CRV rewards on an individual callback are paused with LMCallback.set_killed.
pause
LMCallbackFactory.pause()This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the factory, which is intended to be the Curve DAO.
Pauses the factory, so deploy_lm_callback reverts until it is unpaused. Reverts if the factory is already paused.
Emits: Paused event.
<>Source code▼
- LMCallbackFactory.vy
- ownable.vy (Snekmate 🐍)
- pausable.vy (Snekmate 🐍)
@external
def pause():
"""
@notice Pause new LM Callback deployments
"""
ownable._check_owner()
pausable._pause()
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
event Paused:
account: address
paused: public(bool)
@internal
def _pause():
"""
@dev Triggers the pause state. Note that the contract
must not be paused.
@notice This is an `internal` function without access
restriction.
"""
self._require_not_paused()
self.paused = True
log Paused(account=msg.sender)
@internal
def _require_not_paused():
"""
@dev Throws if the contract is paused.
"""
assert not self.paused, "pausable: contract is paused"
▶Example▼
>>> LMCallbackFactory.pause()
unpause
LMCallbackFactory.unpause()This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the factory, which is intended to be the Curve DAO.
Unpauses the factory to allow new LM Callback deployments again. Reverts if the factory is not paused.
Emits: Unpaused event.
<>Source code▼
- LMCallbackFactory.vy
- ownable.vy (Snekmate 🐍)
- pausable.vy (Snekmate 🐍)
@external
def unpause():
"""
@notice Unpause the factory to allow new LM Callback deployments
"""
ownable._check_owner()
pausable._unpause()
owner: public(address)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
event Unpaused:
account: address
paused: public(bool)
@internal
def _unpause():
"""
@dev Lifts the pause state. Note that the contract
must be paused.
@notice This is an `internal` function without access
restriction.
"""
self._require_paused()
self.paused = False
log Unpaused(account=msg.sender)
@internal
def _require_paused():
"""
@dev Throws if the contract is not paused.
"""
assert self.paused, "pausable: contract is not paused"
▶Example▼
>>> LMCallbackFactory.unpause()
paused
LMCallbackFactory.paused() -> bool: viewGetter for whether the factory is paused. While paused, deploy_lm_callback reverts.
Returns: True if paused (bool).
<>Source code▼
- LMCallbackFactory.vy
- pausable.vy (Snekmate 🐍)
from snekmate.utils import pausable
initializes: pausable
exports: (
...
pausable.paused,
)
paused: public(bool)
▶Example▼
Contract Ownership
The factory uses Snekmate's ownable module. renounce_ownership is intentionally not exported: with a zero owner the blueprint could never be updated again.
owner
LMCallbackFactory.owner() -> address: viewGetter for the owner of the factory. The owner can change the blueprint, pause and unpause deployments, and pause or resume CRV rewards on any LM Callback deployed by this factory.
Returns: owner (address).
<>Source code▼
- LMCallbackFactory.vy
- ownable.vy (Snekmate 🐍)
from snekmate.auth import ownable
initializes: ownable
exports: (
# `renounce_ownership` is intentionally not exported: with a zero owner the
# blueprint could never be updated again
ownable.owner,
ownable.transfer_ownership,
...
)
owner: public(address)
▶Example▼
transfer_ownership
LMCallbackFactory.transfer_ownership(new_owner: address)This contract makes use of a Snekmate module to manage roles and permissions. This specific function can only be called by the current owner of the factory.
Transfers ownership of the factory to new_owner. The new owner cannot be the zero address. Because LM Callbacks read the factory owner when set_killed is called, this also hands over the right to pause CRV rewards on every callback deployed by this factory.
| Input | Type | Description |
|---|---|---|
new_owner | address | Address of the new owner. |
Emits: OwnershipTransferred event.
<>Source code▼
- LMCallbackFactory.vy
- ownable.vy (Snekmate 🐍)
exports: (
ownable.owner,
ownable.transfer_ownership,
...
)
event OwnershipTransferred:
previous_owner: indexed(address)
new_owner: indexed(address)
owner: public(address)
@external
def transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice Note that this function can only be
called by the current `owner`. Also,
the `new_owner` cannot be the zero address.
@param new_owner The 20-byte address of the new owner.
"""
self._check_owner()
assert new_owner != empty(address), "ownable: new owner is the zero address"
self._transfer_ownership(new_owner)
@internal
def _check_owner():
"""
@dev Throws if the sender is not the owner.
"""
assert msg.sender == self.owner, "ownable: caller is not the owner"
@internal
def _transfer_ownership(new_owner: address):
"""
@dev Transfers the ownership of the contract
to a new account `new_owner`.
@notice This is an `internal` function without
access restriction.
@param new_owner The 20-byte address of the new owner.
"""
old_owner: address = self.owner
self.owner = new_owner
log OwnershipTransferred(previous_owner=old_owner, new_owner=new_owner)
▶Example▼
>>> LMCallbackFactory.transfer_ownership("0xNewOwner...")
Other Methods
version
LMCallbackFactory.version() -> String[5]: viewGetter for the version of the contract.
Returns: contract version (String[5]).
<>Source code▼
version: public(constant(String[5])) = "1.0.0"