tETH (Ethereum) technical analysis
Summary
This is a technical analysis of all the smart contracts of the asset and its main dependencies.
Disclosure: This is not an exhaustive security review of the asset like the ones done by the Treehouse 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
tETH is a liquid staking token that accrues interest from ETH staking and ETH leveraging staking strategies in DeFi protocols like Aave and Lido, and it will be adding other lending markets. Users deposit ETH or LSTs (currently wstETH) and receive the tETH token. They can redeem the tETH for wstETH after 7 days or immediately by paying a fee.
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 for the underlying ETH.
- A recommendation of pricing strategy to be used in the integration of the asset <> Aave.
- Any miscellaneous aspect of the code we can consider important.
- 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 a 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 | If misused or exploited, it can cause malfunction and/or minor financial losses. 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
- A layered system where most contracts are non-upgradeable and use well-known dependencies and libraries from OZ.
- The tETH contract itself is an upgradable contract by a Safe 5-of-7.
- The system access control relies mostly on a Safe 5-of-7.
Contracts
The following is a non-exhaustive overview of the main smart contracts involved with tETH.
tETH
The so-called tETH is an ERC4626 token a user receives after depositing ETH and LSTs via the router contract. This contract holds the Internal Accounting Unit (IAU) token, which is used for virtual accounting, and only whitelisted contracts can interact by minting/redeeming it. It is a UUPS upgradeable contract by a Safe 5-of-7.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | upgradeAndCall | CRITICAL | ![]() |
owner: Safe 5-of-7 | transferOwnership, updateBlacklister | HIGH | ![]() |
not assigned | blacklist, unBlacklist | HIGH | ![]() |
TreehouseRouter, TreehouseAccounting, TreehouseRedemptionV2, TreehouseFastlane | deposit, mint, withdraw, redeem | HIGH | ![]() |
- Access Control
- The contract relies on the OZ Ownable2Step contract and has its owner set to a Safe 5-of-7.
- The owner can set a blacklister address via the
updateBlackLister(address)
function. - The blacklister (not assigned) can ban addresses by adding to the blacklist via the
blacklist(address)
function, which disables transfers for and from those addresses. The blacklister can also remove from the blacklist via the unBlacklist(address) function. - Addresses allowed to mint and burn tETH are fetched from the IAU contract via the
IAU.isMinter(address)
function.
- Minting and redeeming
- Allowed contracts can deposit the underlying asset of tETH (IAU Token) via the
deposit(amount, to)
function. An alternative is themint(amount, to)
function. - Allowed contract can redeem tETH via the
redeem(amount, to, from)
function. An alternative is thewithdraw(amount, to, from)
function.
- Allowed contracts can deposit the underlying asset of tETH (IAU Token) via the
- Exchange Rate
- As an ERC4626, tETH uses the internal
convertToAssets(shares)
function to calculate the current tETH exchange rate.
- As an ERC4626, tETH uses the internal
InternalAccountingUnit
The IAU contract is an ERC20 token that represents the underlying asset of the tETH ERC4626 and is used for virtual accounting. Only whitelisted minters can interact with this token. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7, Timelock: (not assigned) | transferOwnership, addMinter, removeMinter, setTimelock | HIGH | ![]() |
TreehouseRouter, TreehouseAccounting, TreehouseRedemptionV2, TreehouseFastlane | burn, burnFrom, mintTo, | HIGH | ![]() |
- Access Control
- The contract uses the OZ Ownable2Step contract and has overridden the internal
_checkOwner()
function to include a Timelock. Currently, the owner is set to a Safe 5-of-7, and the Timelock is not assigned. - The owner/timelock can add or remove minters via
addMinter(address)
andremoveMinter(address)
functions, respectively. - The owner/timelock can set the Timelock via the
setTimelock(address)
function. - Only minters can interact with this token via the
mintTo(to, amount)
,burn(amount)
,burnFrom(from, amount)
functions. The contract also checks whether the receiver address of the IAU token is a minter, ensuring that only minters can hold IAU.
- The contract uses the OZ Ownable2Step contract and has overridden the internal
TreehouseRouter
The Router contract is the main entrance for users to mint tETH by depositing ETH and listed LSTs. It interacts directly with the IAU token and tETH ERC4626 by minting IAU to the tETH contract and minting tETH to users. It’s a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | transferOwnership, setDepositCap, setPause, updateRescuer | HIGH | ![]() |
not assigned | rescueETH, rescueERC20 | HIGH | ![]() |
- Access Control
- The contract uses the OZ Ownable contract and is set to a Safe 5-of-7.
- The owner can set the cap in ETH by calling the
setDepositCap(amount)
function. Currently, the ETH cap is set to100000 ETH
. - The owner can set the rescuer address by calling the
updateRescuer(address)
function. - The owner can pause/unpause deposits via the
setPause(bool)
function. - The rescuer can rescue ERC20 tokens and ETH in this contract via the
rescueERC20(token, to, amount)
andrescueETH(to)
functions, respectively.
- Minting
- Users can deposit ETH via the
depositETH(msg.value)
function. Internally, the router converts ETH into wsETH and sends the fresh wsETH to the Vault contract. Then, it calls an internal_mintAndStake(amount, to)
function that mints IAU tokens and deposits them in the tETH contract. It finishes sending the tETH to the user and validating that the deposit cap wasn’t exceeded. - It is also possible to directly deposit wstETH via the
deposit(asset, amount)
function, which behaves the same way as the depositETH; however, it skips the conversion to wstETH.
- Users can deposit ETH via the
RedemptionController
This contract manages redemptions from the different redemption contracts composed in the system. It interacts directly with the Vault contract by transferring the underlying asset (wstETH) when redemption contracts request redemption. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | addRedemption, removeRedemption, setPauser, transferOwnership | HIGH | ![]() |
TreehouseRedemptionV2, TreehouseFastlane | redeem | HIGH | ![]() |
owner: Safe 5-of-7, pauser: not assigned | setPause | HIGH | ![]() |
- Access Control
- The owner can add new redemption contracts or remove existing ones by calling the addRedemption(address) and removeRedemption(address) functions, respectively.
- The owner can set the pauser address via the
setPauser(address)
function. Both the owner and the pauser can pause/unpause redemptions by calling the setPause(bool) function. - The redemption contracts can redeem wstETH by calling the
redeem(amount,recipient)
function, which transfers theamount
from the Vault contract to therecipient
address.
TreehouseRedemptionV2
The TreehouseRedemptionV2 contract allows users to redeem tETH for wstETH after a 7-day waiting period. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | setMinRedeem, setWaitingPeriod, setRedemptionFee, transferOwnership | HIGH | ![]() |
- Access Control
- The owner can set the minimum amount that can be redeemed instantly via the
setMinRedeem(amount)
function. Currently, it is set to200 wstETH
. - The owner can set the minimum waiting period via the
setWaitingPeriod(secs)
function. Currently, it is set to7 days
. - The owner can set the fee applied during the redemption by calling the
setRedemptionFee(bps)
function. Currently set to0.05%
.
- The owner can set the minimum amount that can be redeemed instantly via the
- Redemption
- Users initiate a redemption by calling the
redeem(shares)
function. The user is included in a redemption queue with the information regarding the amount of wsETH (calculated using thetETH.previewRedeem(shares)
method) and the request’s start time. - After 7 days, users can finalize the redemption by calling the
finalizeRedeem()
function, which withdraws the wstETH from the Vault via theredemptionController.redeem(amount, user)
function, transferring the wstETH to the user
- Users initiate a redemption by calling the
TreehouseFastlane
The TreehouseFastlane contract enables users who want to redeem tETH instantly by paying a fee. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | setMinRedeem, setFeeContract, transferOwnership | HIGH | ![]() |
- Access Control
- The owner can set the minimum amount that can be redeemed instantly via the
setMinRedeem(amount)
function. Currently, it is set to0
. - The owner can set the fee contract that calculates and applies the fee for instant redemption via the setFeeContract. Currently, it is set to the FastlaneFee contract, and the fee set is
2%
.
- The owner can set the minimum amount that can be redeemed instantly via the
- Fast Redeem
- Users can redeem tETH instantly by calling the
redeemAndFinalize(amount)
function. Internally, it verifies if the Vault contract has a sufficient amount to cover the redemption, and if so, it applies the 2% fee and calls theredemptionController.redeem(amount, user)
function, transferring the wstETH to the user
- Users can redeem tETH instantly by calling the
PnlAccounting
The PnlAccounting contract is responsible for calculations that directly reflect the exchange rate of tETH. It calculates the profit and loss of assets locked in the Vault contract and deployed on the strategies contracts. Then, in a cooldown interval, it sends the amount to be minted or burned, plus fees, to the TreehouseAccounting contract via an executor, which updates the internal accounting (IAU token). It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | transferOwnership, setPauser, setCooldownSeconds, updateExecutor, setDeviation | HIGH | ![]() |
Safe 5-of-7, Safe 1-of-5 | doAccounting | HIGH | ![]() |
- Access Control
- The owner can set the executor address by calling the
updateExecutor(address)
function. - The owner can update the cooldown interval via the setCooldownSeconds(seconds) function.
- The owner can set the deviation of the maximum PnL threshold per accounting window by calling the
setDeviation(bps)
. Currently, the deviation is set to0.025%
. - The owner can pause/unpause the contract by calling the
setPauser(bool)
function.
- The owner can set the executor address by calling the
- Exchange Rate update
- The owner or the executor can initiate the internal accounting (reflecting the tETH exchange rate) by calling the
doAccounting()
function. Internally, it calls the NavLens helper contract to fetch the amount of IAU tokens held by the tETH Vault contract (lastNav
) and the current amount of tokens in terms of wstETH held by the Vault contract, plus the amounts in use by strategies (currentNav
). If the PnL is positive (currentNav > lastNav
), the function then callsTreehouseAccounting.mark(type, amount, fee)
, indicating a mint request plus fees based on the difference between the current and last NAV values. Otherwise, it flags as a burn of IAU tokens. - It is important to mention that when calculating the
lastNav
(the current amount of tokens in terms of wstETH held by the Vault contract, plus the amounts in strategies), the NAVLens helper contract employs thetoken.balanceOf(vault)
function. This approach could potentially expose the system to donation attacks. However, given thedeviation
variable in place, if the netPNL between thecurrentNav
andlastNav
is bigger than the lastNav * deviation, the exchange rate update reverts, acting as a safeguard from this kind of attack.
- The owner or the executor can initiate the internal accounting (reflecting the tETH exchange rate) by calling the
TreehouseAccounting
The TreehouseAccounting contract updates the internal accounting (IAU token) with the amounts plus fees calculated by the PnlAccounting contract. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | transferOwnership, updateExecutor, updateTreasury, setFee | HIGH | ![]() |
Safe 5-of-7, PnlAccounting | mark | HIGH | ![]() |
- Access Control
- The owner can set the executor address by calling the
updateExecutor(address)
function. - The owner can set the treasury address via the
updateTreasury(address)
function. - The owner can set the Fee in bps via the
setFee(fee)
function. - The owner or the executor (the PnlAccounting contract) can update the internal accounting (IAU token) by calling the
mark(type, amount, fee)
function, wheretype
means to mint or burn IAU tokens. If it marks to mint, it mints theamount
of IAU tokens, sends them to the tETH vault, and mints thefee
value in tETH to the treasury. Otherwise, it just burns theamount
of IAU tokens locked in the tETH vault.
- The owner can set the executor address by calling the
Vault
The vault contract is responsible for holding assets from the system that were not deposited into the strategies. It is a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | setStrategyStorage, setRedemption, addAllowableAsset, removeAllowableAsset, transferOwnership, updateRescuer | HIGH | ![]() |
not assigned | rescueETH, rescueERC20 | HIGH | ![]() |
tETH Strategy | withdraw | HIGH | ![]() |
- Access control
- The owner can set the StrategyStorage contract via the
setStrategyStorage(address)
function. - The owner can set the Redemption contract via the
setRedemption(address)
function. Internally, this function also max approves the redemption contract, enabling it to pull the underlying asset (wsETH) from the Vault. Currently, it is set to the RedemptionController contract. - The owner can add and remove assets that can be withdrawn from the Vault via the
addAllowableAsset(address)
andremoveAllowableAsset(address)
functions, respectively. - Active strategies in the StrategyStorage Contract can withdraw allowed assets via the
withdraw(asset, amount)
function.
- The owner can set the StrategyStorage contract via the
StrategyStorage
This contract stores the addresses of each strategy contract implemented in the system and the actions it can perform, such as supplying and borrowing assets from Aave (leverage wstETH strategy) and other lending markets soon. The actions are selectors that retrieve smart contracts stored in the ActionsRegistry contract, which contain the code necessary to execute. It also limits which assets each strategy can use during an action. It’s a non-upgradeable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | storeStrategy, whitelistActions, unwhitelistActions, whitelistAssets, unwhitelistAssets, pauseStrategy, unpauseStrategy, setStrategyExecutor | HIGH | ![]() |
- Access Control
- The owner can add strategies via the
storeStrategy(address, actions[], assets[])
function. It needs to specify which actions can be performed and for which assets. - The owner can add and remove actions for different strategies via the
whitelistActions(id, actions[])
andunwhitelistActions(id, actions[])
functions, respectively. - The owner can add and remove assets for different strategies via the
whitelistAssets(id, assets[])
andunwhitelistAssets(id, assets[])
functions, respectively. - The owner can pause/unpause strategies via the
pauseStrategy(id)
andunpauseStrategy(id)
functions. - The owner can update the StrategyExecutor address by calling the
setStrategyExecutor(address)
function.
- The owner can add strategies via the
StrategyExecutor
The StrategyExecutor contract is the starting point for executors to initiate actions on strategies, giving the strategy ID and the list of actions, calldata, and parameters necessary to perform them. It’s a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | updateExecutor | HIGH | ![]() |
- | executeOnStrategy⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | HIGH | ![]() |
- Access control
- The owner can add or remove executors of actions via the
updateExecutor(address, bool)
function. - Allowed executors can initiate actions on strategies via the
executeOnStrategy(id, actions[], calldata[], paramsMapping[])
function, passing the strategyid
, list ofactions
, theircalldata
, and param mappings.
- The owner can add or remove executors of actions via the
- Executing Actions
- After calling
executeOnStrategy()
the StrategyExecutor will validate in the StrategyStorage if strategy is active and if the actions passed are available for that strategy, if everything is correct, the executor callsstrategy.callExecute()
function which initiates the execution of the actions.
- After calling
Strategy
The Strategy Contract is a general contract in which actions are performed via delegate call. It uses the storage and assets held by this contract. New strategies can be deployed and added or removed from the StrategyStorage contract. It’s a non-upgradeable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
StrategyExecutor | callExecute⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ | HIGH | ![]() |
- Executing Actions
- Only the StrategyExecutor contract initiates the actions on the Strategy contract via the
callExecute(target, data)
function. It passes astarget
the ActionsExecutor contract, which will perform delegate calls on each action contract via theactionExecutor.executeActions(actionsIds[], actionCalldata[], paramMapping[])
function.
- Only the StrategyExecutor contract initiates the actions on the Strategy contract via the
ActionsExecutor
The ActionsExecutor is a stateless contract that performs via delegate call actions initiated on the Strategy contract. It’s a non-upgradable contract.
- Executing Actions
- The Strategy contract initiates the execution of actions by calling the
actionExecutor.executeActions(actionsIds[], actionCalldata[], paramMapping[])
function, which fetches each action address registered in the ActionsRegistry contract via theactionRegistry.getAddr(actionId)
, and then, via its internaldelegateCallAndReturnBytes32(target, data)
, it performs a delegatecall (using the storage from the Strategy contract) by calling theaction.executeAction(calldata)
function.
- The Strategy contract initiates the execution of actions by calling the
ActionsRegistry
The ActionsRegistry contract stores all action addresses and capabilities for switching action addresses and rolling back to the previous one if necessary. It’s a non-upgradable contract.
Permission Owner | functions | Criticality | Risk |
---|---|---|---|
owner: Safe 5-of-7 | addNewContract, revertToPreviousAddress, startContractChange, approveContractChange, cancelContractChange | HIGH | ![]() |
- Access Control
- The owner can add new actions contracts via the
addNewContract(id,address)
function. - The owner can change a previous action contract via the
startContractChange(id address)
function. This function flags the actionid
as in change and sets the new contract as pending. - The owner can finalize the action contract change via the
approveContractChange(id)
function, which will remove the in-change flag for theid
, set the final action address using the pending one, and keep the previous action address in thepreviousAddresses
mapping if needed to roll back. - The owner can cancel the action contact change via the
cancelContractChange(id)
, setting the pending address toaddress(0)
, and removing the in-change flag. - The owner can roll back to the previous action address via the
revertToPreviousAddress(id)
function. It will replace the current actionid
address with the address stored in thepreviousAddresses[id]
mapping.
- The owner can add new actions contracts via the
Pricing strategy
Our suggestion for pricing tETH is using a CAPO tETH/ETH adapter with the tETH Vault providing the exchange rate with a CL WETH/USD feed, which follows the procedure of pricing LSTs on Aave. We can confirm that the exchange rate provided by the tETH Vault is safeguarded against possible manipulations.
Miscellaneous
- The system has undergone security reviews by Trail of Bits, Sigma Prime, Fuzzland, Omniscia, and BlockSec. They can be found here. Also, there is an ongoing bug bounty program; more information can be found here.
- The system relies heavily on its owner (Safe wallet 5-of-7), which poses risks of centralization. If the signer’s wallet gets compromised or if malicious transactions are signed without their knowledge, various critical components of the system could be at risk. We recommend that the team implement a Timelock as the system’s owner, allowing sufficient time to validate any changes made within it.
- Treehouse intends to expand tETH with an underlying strategy using the Compound protocol for wstETH/WETH leverage.
Conclusion
Given the anticipated listing nature of the asset as collateral, we requested the Treehouse Team to timelock different parts of the system for security reasons, like upgradeability. The team has agreed to and will implement these requested changes, but it is a technical blocker.
We also don’t think it’s acceptable from a technical risk standpoint to have underlying strategies that rely on non-Aave lending protocols (e.g., aforementioned Compound), as this creates overly complex dependencies on systems that are not even aligned with Aave. However, community feedback on the topic is welcome.