[ARFC] Enable eBTC/WBTC liquid E-Mode on Aave v3 Core Instance

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
:green_circle: 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.
:yellow_circle: The role is controlled in a way that could expose the system and users to some risk depending on the actions it can control.
:red_circle: 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 :green_circle:
OWNER_ROLE: Timelock (0x70a6…cE3d) setBeforeTransferHook, setAuthority, transferOwnership CRITICAL :green_circle:
MANAGER_ROLE: (Manager Contract) manage CRITICAL :green_circle:
MINTER_ROLE: Teller contract enter HIGH :green_circle:
BURNER_ROLE: Teller contract exit HIGH :green_circle:
  • 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 the token amount from the user and mints the shareAmount to the user.
    • The Teller contract (BURNER_ROLE) is the one that can burn eBTC via the exit(ERC20 token, amount, shareAmount) function. Internally, this function burns the shareAmount and transfers the amount of the token to the user.
  • Access Control
    • The OWNER_ROLE can set a before transfer hook contract via the setBeforeTransferHook(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 the manage(target, data, value) function. The manager contract whitelists the target data and value via a Merkle proofs system (detailed in the Manager section).
  • 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 the from, to or operator are on the deny list.

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 :green_circle:
OWNER_ROLE: Timelock (0x70a6…cE3d) setShareLockPeriod, updateAssetData HIGH :green_circle:
OWNER_ROLE (bridge functions) Timelock (0x70a6…cE3d) addChain, allowMessagesFromChain, allowMessagesToChain, setChainGasLimit HIGH :green_circle:
STRATEGIST_MULTISIG_ROLE: Safe 2-of-3 (0x41DF…a6ae) and Safe 2-of-3 (0x71E2…Bcd6) updateAssetData, refundDeposit HIGH :yellow_circle:
MULTISIG_ROLE: Safe 4-of-6, Timelock (0x70a6…cE3d) pause, unpause, removeChain, stopMessagesFromChain, stopMessagesToChain, setOutboundRateLimits, setInboundRateLimits, HIGH :green_circle:
SOLVER_ROLE: AtomicSolverV3, LBTCv, BoringSolver, BoringSolver(2), Liquid Bera BTC, LiquidBTC bulkDeposit, bulkWithdraw HIGH :green_circle:
pauser: pauser contract, Safe 4-of-6 pause, unpause HIGH :green_circle:
DENIER_ROLE (not assigned) denyAll, allowAll, denyFrom, allowFrom, denyTo, allowTo, denyOperator, allowOperator MEDIUM :green_circle:
  • 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 the getRateInQuoteSafe(asset) function. The deposit reverts if the accountant does not support the asset. Finally, the Teller calls the enter(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 token in (eBTC) and the token out (wBTC, cbBTC or LBTC) and a request struct containing the deadline for the withdrawal to be completed, a discount value, and the number of shares they want to redeem. After the period has passed, the redemption initiates in one transaction via the redeemSolve() function and is detailed in the steps below:
      1. A redeemSolve(ERC20 in, ERC20 out, address[] users) function is called in the helper AtomicSolver contract, batching users’ requests that have chosen the same token out.
      2. Internally, the AtomicSolver calls AtomicQueue.solve(ERC20 in, ERC20 out, address[] users), which transfers the token in from the users to the AtomicSolver and calls the finishSolve(ERC20 in, ERC20 out, totalAmount) fallback function.
      3. The finishSolve() fallback function then calls the Teller to redeem the shareAmount of eBTC via the bulkWithdraw(ERC20 out, uint256 shareAmount, address to) function.
      4. 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 the shareAmount of eBTC and sends the amount to the AtomicQueue.
      5. The atomicQueue then finishes by sending the requested token amount to each user.
  • 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 the BoringVault.exit() function to burn the shares to the zero address. Then, the message data is built with the amount, the to address, and the bridgeWildCard, which contains the destination chain information. Users can back and forth their eBTC through Arbitrum, Base, Corn, and mainnet.
  • 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, and SOLVER_ROLE.
    • The OWNER_ROLE can configure new chains in the bridge system by adding via the addChain() function, allowing sending and receiving messages from and to the chain via the allowMessagesFromChain() allowMessagesToChain() functions and set the gas limit for the respectively chain by calling setChainGasLimit().
    • The OWNER_ROLE and the STRATEGIST_MULTISIG_ROLE can add or remove assets via the updateAssetData(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 the setShareLockPeriod(period) function.
    • The OWNER_ROLE, and the DENIER_ROLE can add or remove users and operators in the blacklist of sending or receiving eBTC via the allowAll(), denyAll(), allowFrom(), denyFrom(), allowTo(), denyTo(), denyOperator(), allowOperator() functions.
    • The MULTISIG_ROLE can configure in and outbound rate limits for bridging via the setInboundRateLimits(config) and setOutboundRateLimits(config) functions.
    • The MULTISIG_ROLE can stop messages and remove chains via the stopMessagesFromChain(chainId), stopMessagesToChain(chainId), removeChain(chainId) functions.
    • The MULTISIG_ROLE can pause and unpause the Teller contract via the pause() and unpause() functions.
    • The SOLVER_ROLE (the helper contract) can redeem eBTC via the bulkWithdraw() function. It also can deposit assets and mint eBTC via the bulkDeposit() function, which works in a similar way of the normal deposit() function.

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 :green_circle:
UPDATE_EXCHANGE_RATE_ROLE: (not assigned) updateExchangeRate CRITICAL :green_circle:
OWNER_ROLE: Timelock (0x70a6…cE3d) updateDelay, updateUpper, updateLower, updateManagementFee, updatePerformanceFee, updatePayoutAddress, setRateProviderData, resetHighwaterMark, HIGH :green_circle:
MULTISIG_ROLE: Safe 4-of-6 (0xCEA8…ec96) pause, unpause, HIGH :green_circle:
pauser: pauser contract pause, unpause, HIGH :green_circle:
  • Exchange Rate
    • The UPDATE_EXCHANGE_RATE_ROLE set the new exchange rate via the updateExchangeRate(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 and performanceFee 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 the claimFees() function.
  • 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 the setRateProviderData(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 the updateUpper(value) and updateLower(value) functions.
    • The OWNER_ROLE can set the minimum time delay between the rate updates via the updateDelay(value) function.
    • The OWNER_ROLE can set the management and performance fees and the address to send them via the updateManagementFee(value), updatePerformanceFee(value) and updatePayoutAddress(address) functions.
    • The OWNER_ROLE can set the high watermark to the current exchange rate via the resetHighwaterMark() function.
    • The MULTISIG_ROLE can pause and unpause via the pause() and unpause() functions.

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 :green_circle:
OWNER_ROLE: Timelock (0x70a6…cE3d) setManageRoot CRITICAL :green_circle:
STRATEGIST_ROLE (not assigned) manageVaultWithMerkleVerification HIGH :green_circle:
MANAGER_INTERNAL_ROLE: Manager Contract manageVaultWithMerkleVerification HIGH :green_circle:
MICRO_MANAGER_ROLE: SymbioticUManager,Safe 2-of-3 (0x41DF…a6ae), Safe 2-of-3 (0x71E2…Bcd6) manageVaultWithMerkleVerification HIGH :yellow_circle:
MULTISIG_ROLE: Safe 4-of-6 (0xCEA8…ec96) pause, unpause HIGH :green_circle:
pauser: pauser contract pause, unpause, HIGH :green_circle:
  • Rebalance
    • Rebalances are shared between the MANAGER_INTERNAL_ROLE, STRATEGIST_ROLE and MICRO_MANAGER_ROLE via the manageVaultWithMerkleVerification(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 the manageRoot contains all the authorized operations the strategist (msg.sender) can do in the BoringVault and the sanitizer is the address used to obtain the arguments in the targetData (packedArgumentAddresses). With that in hands, the _verifyManageProof(manageRoot, manageProof, target, sanitizer, value, selector, packedArgumentAddresses) function is called to calculate the leaf of the current operation by hashing the parameters via keccak256 and verifying via the MerkleProofLib.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 the Manager.flashLoan() function). The receiveFlashLoan() callback function is in the Manager contract to complete the operation. The Manager contract enforces that the totalSupply of eBTC cannot change after a rebalance completes.
  • Access control
    • The OWNER_ROLE can set the rebalancing data of each strategist via the setManageRoot(address, bytes manageRoot). The manageRoot (the root of the Merkle tree) is encoded data containing a address decodersAndSanitizer to call and extract packed address arguments from the calldata; a address target, bool valueIsNonZero indicating whether or not the value is non-zero, bytes 4 selector the function selector, and the argumentAddress is each allowed address argument in that call.
    • The MULTISIG_ROLE can pause and unpause via the pause() and unpause() functions.


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.

2 Likes