eBTC (Ether.fi) technical analysis
Summary
This is a technical analysis of all the smart contracts of the asset and main dependencies.
Disclosure: This is not an exhaustive security review of the asset like the ones done by the Ether.fi Team, but an analysis from an Aave technical service provider on different aspects we consider critical to review before a new type of listing. Consequently, like with any security review, this is not an absolute statement that the asset is flawless, only that, in our opinion, we don’t see significant problems with its integration with Aave, apart from different trust points.
Analysis
eBTC is a BTC liquid restaking token allowing users to earn staking and restaking yield across DeFI applications.
eBTC is backed by LBTC, wBTC, and cbBTC, restaked partially or totally in Eigen Layer, Symbiotic, and Karak, with rewards coming from Babylon.
eBTC uses as the core smart contract the BoringVault
by Veda, but with the access control of the system majorly managed by Ether.fi itself.
Summarised architecture high-level
For the context of this analysis, our focus has been on the following aspects, critical for the correct and secure integration with Aave:
- Mechanism to update the exchange rate of the asset with the underlying/s.
- A recommendation of pricing strategy to be used in the integration asset <> Aave.
- Any miscellaneous aspect of the code we can consider of importance.
- Analysis of the access control (ownerships, admin roles) and the nature of the entities involved in the system. Regarding the table permissions’ holders and their criticality/risk, it is done following these guidelines:
Criticality | Description |
---|---|
CRITICAL | Usually super-admin functionality: it can compromise the system by completely changing its fundamentals, leading to loss of funds if misused or exploited. E.g. proxy admin, default admin |
HIGH | It can control several parts of the system with some risk of losing funds. E.g., general owners or admin roles involved in the flow of funds |
MEDIUM | It can cause malfunction and/or minor financial losses if misused or exploited. E.g., fee setter, fee recipient addresses |
LOW | It can cause system malfunctions but on non-critical parts without meaningful/direct financial losses. E.g., updating descriptions or certain non-critical parameters. |
Risk | Description |
---|---|
![]() |
The role is controlled via a mechanism we consider safe, such as on-chain governance, a timelock contract, or setups involving multi-sigs under certain circumstances. |
![]() |
The role is controlled in a way that could expose the system and users to some risk depending on the actions it can control. |
![]() |
The role is controlled via a clearly non-secure method, representing risks for the system and users. |
General points
- The main contracts are non-upgradable, using solmate Auth pattern for access control and LayerZero contracts for bridging.
- The general admin of the system is an Authority contract controlled by a 5-day Timelock.
Contracts
The following is a non-exhaustive overview of the main smart contracts involved with eBTC.
eBTC (BoringVault)
The eBTC contract (BoringVault) is a simple contract holding the deposited assets through the Teller contract. It has functionalities for a Manager to send arbitrary calls (but pre-whitelisted) via the eBTC contract, mainly used for rebalancing strategies. It’s a non-upgradable contract with role-based access control via the Authority contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
general owner: Authority → Timelock (0x70a6…cE3d) | setAuthority, setRoleCapability, setPublicCapability, setUserRole, transferOwnership | CRITICAL | ![]() |
OWNER_ROLE : Timelock (0x70a6…cE3d) |
setBeforeTransferHook, setAuthority, transferOwnership | CRITICAL | ![]() |
MANAGER_ROLE : (Manager Contract) |
manage | CRITICAL | ![]() |
MINTER_ROLE : Teller contract |
enter | HIGH | ![]() |
BURNER_ROLE : Teller contract |
exit | HIGH | ![]() |
- Mint and Burn
- minting eBTC happens via the
enter(ERC20 token, amount, shareAmount)
function which is only called by the Teller contract (MINTER_ROLE
). This function transfers thetoken
amount
from the user and mints theshareAmount
to the user. - The Teller contract (
BURNER_ROLE
) is the one that can burn eBTC via theexit(ERC20 token, amount, shareAmount)
function. Internally, this function burns theshareAmount
and transfers theamount
of thetoken
to the user.
- minting eBTC happens via the
- Access Control
- The
OWNER_ROLE
can set a before transfer hook contract via thesetBeforeTransferHook(address)
function. Currently, the hook is the Teller contract. - The
MANAGER_ROLE
(Manager Contract)) can rebalance the vault by making arbitrary calls via the eBTC contract by calling themanage(target, data, value)
function. The manager contract whitelists thetarget
data
andvalue
via a Merkle proofs system (detailed in the Manager section).
- The
- Tranfers (Before transfer hook)
- eBTC has a share lock period implemented in the Teller contract that turns off temporary transfers if the
currentShareLockPeriod
is a value greater than zero. However, it is not currently set. - It also presents
fromDenyList
toDenyList
operatorDenyList
mappings that blocks transfers if thefrom
,to
oroperator
are on the deny list.
- eBTC has a share lock period implemented in the Teller contract that turns off temporary transfers if the
Teller (LayerZeroTellerWithRateLimiting)
The Teller contract is the main entrance for users to mint eBTC directly from the BoringVault. Withdrawals are processed in the Teller contract but managed via two helper contracts (Queue and Solver). The Teller contract uses the Accountant to calculate the exchange rate of the supported assets (currently LBTC, wBTC, and cbBTC). It has integrated with LayerZero to bridge eBTC through supported chains. It’s a non-upgradable contract with role-based access control via the Authority contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
general owner: Authority → Timelock (0x70a6…cE3d) | setAuthority, setRoleCapability, setPublicCapability, setUserRole, transferOwnership | CRITICAL | ![]() |
OWNER_ROLE : Timelock (0x70a6…cE3d) |
setShareLockPeriod, updateAssetData | HIGH | ![]() |
OWNER_ROLE (bridge functions) Timelock (0x70a6…cE3d) |
addChain, allowMessagesFromChain, allowMessagesToChain, setChainGasLimit | HIGH | ![]() |
STRATEGIST_MULTISIG_ROLE : Safe 2-of-3 (0x41DF…a6ae) and Safe 2-of-3 (0x71E2…Bcd6) |
updateAssetData, refundDeposit | HIGH | ![]() |
MULTISIG_ROLE : Safe 4-of-6, Timelock (0x70a6…cE3d) |
pause, unpause, removeChain, stopMessagesFromChain, stopMessagesToChain, setOutboundRateLimits, setInboundRateLimits, | HIGH | ![]() |
SOLVER_ROLE : AtomicSolverV3, LBTCv, BoringSolver, BoringSolver(2), Liquid Bera BTC, LiquidBTC |
bulkDeposit, bulkWithdraw | HIGH | ![]() |
pauser: pauser contract, Safe 4-of-6 | pause, unpause | HIGH | ![]() |
DENIER_ROLE (not assigned) |
denyAll, allowAll, denyFrom, allowFrom, denyTo, allowTo, denyOperator, allowOperator | MEDIUM | ![]() |
- Mint and Burn
- Users can mint eBTC by calling the
deposit(asset, amount)
function. Internally, the function calculates the number of shares based on the amount of the asset deposited using the Accountant contract by calling thegetRateInQuoteSafe(asset)
function. The deposit reverts if the accountant does not support the asset. Finally, the Teller calls theenter(asset, amount)
function in the Boring Vault, and the new eBTC shares are sent to the user. - Users can redeem eBTC in a 7-day window by first giving allowance to a helper AtomicQueue contract and calling the
updateAtomicRequest(ERC20 in, ERC20 out, request)
function where the users specify the tokenin
(eBTC) and the tokenout
(wBTC, cbBTC or LBTC) and a request struct containing thedeadline
for the withdrawal to be completed, adiscount
value, and thenumber of shares
they want to redeem. After the period has passed, the redemption initiates in one transaction via theredeemSolve()
function and is detailed in the steps below:- A
redeemSolve(ERC20 in, ERC20 out, address[] users)
function is called in the helper AtomicSolver contract, batching users’ requests that have chosen the same tokenout
. - Internally, the AtomicSolver calls
AtomicQueue.solve(ERC20 in, ERC20 out, address[] users)
, which transfers the tokenin
from the users to the AtomicSolver and calls thefinishSolve(ERC20 in, ERC20 out, totalAmount)
fallback function. - The
finishSolve()
fallback function then calls the Teller to redeem theshareAmount
of eBTC via thebulkWithdraw(ERC20 out, uint256 shareAmount, address to)
function. - The amount of tokens out in terms of shares is calculated via the Accountant contract. Then the
BoringVault.exit(ERC20 out, amount, shareAmount)
is called which burns theshareAmount
of eBTC and sends theamount
to the AtomicQueue. - The atomicQueue then finishes by sending the requested token amount to each user.
- A
- Users can mint eBTC by calling the
- Exchange Rate
- The exchange rate is calculated through the Accountant contract and will be detailed in the Accountant section.
- Bridge
- Users can natively bridge eBTC using the LayerZero
bridge(shares, to, bridgeWildCard)
function. The function first verifies whether the shares can be transferred and then calls theBoringVault.exit()
function to burn the shares to the zero address. Then, the message data is built with theamount
, theto
address, and thebridgeWildCard
, which contains the destination chain information. Users can back and forth their eBTC through Arbitrum, Base, Corn, and mainnet.
- Users can natively bridge eBTC using the LayerZero
- Access Control
- The Teller has a role-based access control via the Authority contract, with the main roles being
OWNER_ROLE
,MULTISIG_ROLE
,DENIER_ROLE
,STRATEGIST_MULTISIG_ROLE
, andSOLVER_ROLE
. - The
OWNER_ROLE
can configure new chains in the bridge system by adding via theaddChain()
function, allowing sending and receiving messagesfrom
andto
the chain via theallowMessagesFromChain()
allowMessagesToChain()
functions and set the gas limit for the respectively chain by callingsetChainGasLimit()
. - The
OWNER_ROLE
and theSTRATEGIST_MULTISIG_ROLE
can add or remove assets via theupdateAssetData(address, bool deposit, bool withdraw)
function. It introduces some flexibility in the system, for example, allowing certain assets to be only deposited or withdrawn. - The
OWNER_ROLE
can set the lock period of eBTC via thesetShareLockPeriod(period)
function. - The
OWNER_ROLE
, and theDENIER_ROLE
can add or remove users and operators in the blacklist of sending or receiving eBTC via theallowAll()
,denyAll()
,allowFrom()
,denyFrom()
,allowTo()
,denyTo()
,denyOperator()
,allowOperator()
functions. - The
MULTISIG_ROLE
can configure in and outbound rate limits for bridging via thesetInboundRateLimits(config)
andsetOutboundRateLimits(config)
functions. - The
MULTISIG_ROLE
can stop messages and remove chains via thestopMessagesFromChain(chainId)
,stopMessagesToChain(chainId)
,removeChain(chainId)
functions. - The
MULTISIG_ROLE
can pause and unpause the Teller contract via thepause()
andunpause()
functions. - The
SOLVER_ROLE
(the helper contract) can redeem eBTC via thebulkWithdraw()
function. It also can deposit assets and mint eBTC via thebulkDeposit()
function, which works in a similar way of the normaldeposit()
function.
- The Teller has a role-based access control via the Authority contract, with the main roles being
Accountant (AccountantWithRateProviders)
The Accountant contract provides the eBTC exchange rate to the Teller contract and quotes for the different assets that can be deposited. The exchange rate is updated with off-chain data, and the Accountant limits the range it can change. It’s a non-upgradable contract with role-based access control via the Authority contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
general owner: Authority → Timelock (0x70a6…cE3d) | setAuthority, setRoleCapability, setPublicCapability, setUserRole, transferOwnership | CRITICAL | ![]() |
UPDATE_EXCHANGE_RATE_ROLE : (not assigned) |
updateExchangeRate | CRITICAL | ![]() |
OWNER_ROLE : Timelock (0x70a6…cE3d) |
updateDelay, updateUpper, updateLower, updateManagementFee, updatePerformanceFee, updatePayoutAddress, setRateProviderData, resetHighwaterMark, | HIGH | ![]() |
MULTISIG_ROLE : Safe 4-of-6 (0xCEA8…ec96) |
pause, unpause, | HIGH | ![]() |
pauser: pauser contract | pause, unpause, | HIGH | ![]() |
- Exchange Rate
- The
UPDATE_EXCHANGE_RATE_ROLE
set the new exchange rate via theupdateExchangeRate(newRate)
function. - The exchange rate is calculated using off-chain data and includes safeguards that set minimum and maximum acceptable rates (currently a range of 10bps) and minimum update period (currently 6 hours). If the new rate falls outside the defined bounds, the Accountant contract will be paused instead of reverting the update transaction. This also halts new deposits and withdrawals because the Teller needs to call the
getRateInQuoteSafe()
function to calculate the shares being minted/burned, which will revert when the Accountant is paused. - When the exchange rate is updated with an acceptable value, a
managementFee
andperformanceFee
are calculated and accrued to the contract. Currently, the Accountant has zero fees for management and performance. The management fee is calculated based on the time elapsed since the last update using the minimum value of shares, while the performance fee is calculated if the new rate is greater than the previous high watermark (the last exchange rate). The boring vault can claim the fees via theclaimFees()
function.
- The
- Access Control
- The
OWNER_ROLE
can set the rate provider data address of the assets being deposited and withdrawn from the eBTC system and if they should be pegged to the base exchange rate via thesetRateProviderData(ERC20 asset, bool isPeggedToBase ,address rateProvider)
function. - The
OWNER_ROLE
can configure the upper and lower bound of the acceptable exchange rate in bps via theupdateUpper(value)
andupdateLower(value)
functions. - The
OWNER_ROLE
can set the minimum time delay between the rate updates via theupdateDelay(value)
function. - The
OWNER_ROLE
can set the management and performance fees and the address to send them via theupdateManagementFee(value)
,updatePerformanceFee(value)
andupdatePayoutAddress(address)
functions. - The
OWNER_ROLE
can set the high watermark to the current exchange rate via theresetHighwaterMark()
function. - The
MULTISIG_ROLE
can pause and unpause via thepause()
andunpause()
functions.
- The
Manager (ManagerWithMerkleVerification)
The Manager contract is responsible for rebalancing the Boring vault via the manage()
function, which allows the manager to send arbitrary calls. The contract has a role-based access control where the owner must first whitelist the call content of the strategist role (responsible for rebalancing). Then, when a rebalance is initiated, the content is verified via Merkle proofs. The Manager is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
general owner: Authority → Timelock (0x70a6…cE3d) | setAuthority, setRoleCapability, setPublicCapability, setUserRole, transferOwnership | CRITICAL | ![]() |
OWNER_ROLE : Timelock (0x70a6…cE3d) |
setManageRoot | CRITICAL | ![]() |
STRATEGIST_ROLE (not assigned) |
manageVaultWithMerkleVerification | HIGH | ![]() |
MANAGER_INTERNAL_ROLE : Manager Contract |
manageVaultWithMerkleVerification | HIGH | ![]() |
MICRO_MANAGER_ROLE : SymbioticUManager,Safe 2-of-3 (0x41DF…a6ae), Safe 2-of-3 (0x71E2…Bcd6) |
manageVaultWithMerkleVerification | HIGH | ![]() |
MULTISIG_ROLE : Safe 4-of-6 (0xCEA8…ec96) |
pause, unpause | HIGH | ![]() |
pauser: pauser contract | pause, unpause, | HIGH | ![]() |
- Rebalance
- Rebalances are shared between the
MANAGER_INTERNAL_ROLE
,STRATEGIST_ROLE
andMICRO_MANAGER_ROLE
via themanageVaultWithMerkleVerification(proofs[], sanitizers[], targets[], calldata[], values[])
function. - When the strategist initiates a rebalancing, first the Manager verifies if the
calldata
passed is allowed via the_verifyCallData(manageRoot, manageProof, sanitizer, target, value, targetData)
function, where themanageRoot
contains all the authorized operations the strategist (msg.sender
) can do in the BoringVault and thesanitizer
is the address used to obtain the arguments in thetargetData
(packedArgumentAddresses
). With that in hands, the_verifyManageProof(manageRoot, manageProof, target, sanitizer, value, selector, packedArgumentAddresses)
function is called to calculate theleaf
of the current operation by hashing the parameters viakeccak256
and verifying via theMerkleProofLib.verify(proof, root, leaf)
. If the proof is valid, the rebalancing can proceed. - The Manager contract integrates with Balancer flashLoan and must be initiated via the BoringVault to be valid (the strategist calls the
manageVaultWithMerkleVerification()
, and the BoringVault calls theManager.flashLoan()
function). ThereceiveFlashLoan()
callback function is in the Manager contract to complete the operation. The Manager contract enforces that thetotalSupply
of eBTC cannot change after a rebalance completes.
- Rebalances are shared between the
- Access control
- The
OWNER_ROLE
can set the rebalancing data of each strategist via thesetManageRoot(address, bytes manageRoot)
. ThemanageRoot
(the root of the Merkle tree) is encoded data containing aaddress decodersAndSanitizer
to call and extract packed address arguments from the calldata; aaddress target
,bool valueIsNonZero
indicating whether or not the value is non-zero,bytes 4 selector
the function selector, and theargumentAddress
is each allowed address argument in that call. - The
MULTISIG_ROLE
can pause and unpause via thepause()
andunpause()
functions.
- The
Miscellaneous
- The system has 3 security review by 0xMacro and Cantina, and can be found here. We still recommend adding some extra.
- The staking/restaking rewards (Babylon, EigenLayer, and others) are still to be announced, which leaves the eBTC with an exchange rate of 1:1 for all BTC tokens deposited into the eBTC BoringVault.
- Even if the exchange rate relies on off-chain data, internal security mechanisms such as pausing deposits and withdrawals can protect users’ funds during updates when values fall outside the defined range.
- For precaution, we don’t think the asset should be enabled for borrowing, as both pricing and the threat model become relatively complex if so.
Asset Pricing
For LRTs, we have always recommended using a CAPO LRT, combining the exchange rate of the asset (eBTC in this case) to its underlying (BTC), and a Chainlink feed or underlying/USD (BTC/USD) in this case. eBTC is a bit more special than previous cases:
- The eBTC Accountant contract technically considers the WBTC as “base” token
- The majority of the eBTC underlying is staked, finally in the shape of BTC, for example via LBTC. That means that the “real” underlying of eBTC is BTC.
- Currently, it is important to highlight that the rate of eBTC <> WBTC is 1:1 and lacks rewards because both staking and restaking rewards are not live yet. As soon the rewards start accruing, we’ll re-evaluate if the rate providers of the underlying assets are aligned with how they are priced on Aave (in this case, cbBTC and LBTC, which are priced to BTC on Aave).
Considering the previous, one option of pricing is to go the CAPO route, consuming the exchange rate from the Accountant contract plus a Chainlink BTC/USD feed. This way, if rewards are enabled in the future (no plans yet), eBTC on Aave will immediately be aware of them.
On the other hand, it is very consistent with the internal composition of eBTC to simply use BTC/USD, the same as LBTC on Aave. This way, potentially if rewards are enabled Aave would not be aware of them, but with the asset listed as collateral only, that is not a big problem.
In practise, both approaches are pretty much acceptable, but we will sync with both @ChaosLabs and @LlamaRisk to agree on one for the AIP stage.
Conclusion
We think eBTC doesn’t have any problem in terms of integration with Aave, and aside from the necessary proposal payload creation, there is no major technical blocker for listing.